2025-06-15 17:49:27 -04:00
import { db } from "@server/db" ;
import { clients , orgs , sites } from "@server/db" ;
2025-03-25 22:01:08 -04:00
import { and , eq , isNotNull } from "drizzle-orm" ;
import config from "@server/lib/config" ;
2024-10-26 16:04:01 -04:00
interface IPRange {
start : bigint ;
end : bigint ;
2024-10-26 22:44:34 -04:00
}
2025-02-14 12:32:10 -05:00
type IPVersion = 4 | 6 ;
/ * *
* Detects IP version from address string
* /
function detectIpVersion ( ip : string ) : IPVersion {
2025-07-13 21:57:24 -07:00
return ip . includes ( ":" ) ? 6 : 4 ;
2025-02-14 12:32:10 -05:00
}
2024-10-26 22:44:34 -04:00
/ * *
2025-02-14 12:32:10 -05:00
* Converts IPv4 or IPv6 address string to BigInt for numerical operations
2024-10-26 22:44:34 -04:00
* /
function ipToBigInt ( ip : string ) : bigint {
2025-02-14 12:32:10 -05:00
const version = detectIpVersion ( ip ) ;
2025-05-03 12:21:43 -04:00
2025-02-14 12:32:10 -05:00
if ( version === 4 ) {
2025-07-13 21:57:24 -07:00
return ip . split ( "." ) . reduce ( ( acc , octet ) = > {
const num = parseInt ( octet ) ;
if ( isNaN ( num ) || num < 0 || num > 255 ) {
throw new Error ( ` Invalid IPv4 octet: ${ octet } ` ) ;
}
return BigInt . asUintN ( 64 , ( acc << BigInt ( 8 ) ) + BigInt ( num ) ) ;
} , BigInt ( 0 ) ) ;
2025-02-14 12:32:10 -05:00
} else {
// Handle IPv6
// Expand :: notation
let fullAddress = ip ;
2025-07-13 21:57:24 -07:00
if ( ip . includes ( "::" ) ) {
const parts = ip . split ( "::" ) ;
if ( parts . length > 2 )
throw new Error ( "Invalid IPv6 address: multiple :: found" ) ;
const missing =
8 - ( parts [ 0 ] . split ( ":" ) . length + parts [ 1 ] . split ( ":" ) . length ) ;
const padding = Array ( missing ) . fill ( "0" ) . join ( ":" ) ;
2025-02-14 12:32:10 -05:00
fullAddress = ` ${ parts [ 0 ] } : ${ padding } : ${ parts [ 1 ] } ` ;
}
2025-07-13 21:57:24 -07:00
return fullAddress . split ( ":" ) . reduce ( ( acc , hextet ) = > {
const num = parseInt ( hextet || "0" , 16 ) ;
if ( isNaN ( num ) || num < 0 || num > 65535 ) {
throw new Error ( ` Invalid IPv6 hextet: ${ hextet } ` ) ;
}
return BigInt . asUintN ( 128 , ( acc << BigInt ( 16 ) ) + BigInt ( num ) ) ;
} , BigInt ( 0 ) ) ;
2025-02-14 12:32:10 -05:00
}
2024-10-26 22:44:34 -04:00
}
/ * *
* Converts BigInt to IP address string
* /
2025-02-14 12:32:10 -05:00
function bigIntToIp ( num : bigint , version : IPVersion ) : string {
if ( version === 4 ) {
const octets : number [ ] = [ ] ;
for ( let i = 0 ; i < 4 ; i ++ ) {
octets . unshift ( Number ( num & BigInt ( 255 ) ) ) ;
num = num >> BigInt ( 8 ) ;
}
2025-07-13 21:57:24 -07:00
return octets . join ( "." ) ;
2025-02-14 12:32:10 -05:00
} else {
const hextets : string [ ] = [ ] ;
for ( let i = 0 ; i < 8 ; i ++ ) {
2025-07-13 21:57:24 -07:00
hextets . unshift (
Number ( num & BigInt ( 65535 ) )
. toString ( 16 )
. padStart ( 4 , "0" )
) ;
2025-02-14 12:32:10 -05:00
num = num >> BigInt ( 16 ) ;
}
// Compress zero sequences
let maxZeroStart = - 1 ;
let maxZeroLength = 0 ;
let currentZeroStart = - 1 ;
let currentZeroLength = 0 ;
for ( let i = 0 ; i < hextets . length ; i ++ ) {
2025-07-13 21:57:24 -07:00
if ( hextets [ i ] === "0000" ) {
2025-02-14 12:32:10 -05:00
if ( currentZeroStart === - 1 ) currentZeroStart = i ;
currentZeroLength ++ ;
if ( currentZeroLength > maxZeroLength ) {
maxZeroLength = currentZeroLength ;
maxZeroStart = currentZeroStart ;
}
} else {
currentZeroStart = - 1 ;
currentZeroLength = 0 ;
}
}
if ( maxZeroLength > 1 ) {
2025-07-13 21:57:24 -07:00
hextets . splice ( maxZeroStart , maxZeroLength , "" ) ;
if ( maxZeroStart === 0 ) hextets . unshift ( "" ) ;
if ( maxZeroStart + maxZeroLength === 8 ) hextets . push ( "" ) ;
2025-02-14 12:32:10 -05:00
}
2025-07-13 21:57:24 -07:00
return hextets
. map ( ( h ) = > ( h === "0000" ? "0" : h . replace ( /^0+/ , "" ) ) )
. join ( ":" ) ;
2024-10-26 16:04:01 -04:00
}
2024-10-26 22:44:34 -04:00
}
/ * *
* Converts CIDR to IP range
* /
2025-02-14 15:49:23 -05:00
export function cidrToRange ( cidr : string ) : IPRange {
2025-07-13 21:57:24 -07:00
const [ ip , prefix ] = cidr . split ( "/" ) ;
2025-02-14 12:32:10 -05:00
const version = detectIpVersion ( ip ) ;
2024-10-26 16:04:01 -04:00
const prefixBits = parseInt ( prefix ) ;
const ipBigInt = ipToBigInt ( ip ) ;
2025-05-03 12:21:43 -04:00
2025-02-14 12:32:10 -05:00
// Validate prefix length
const maxPrefix = version === 4 ? 32 : 128 ;
if ( prefixBits < 0 || prefixBits > maxPrefix ) {
throw new Error ( ` Invalid prefix length for IPv ${ version } : ${ prefix } ` ) ;
}
const shiftBits = BigInt ( maxPrefix - prefixBits ) ;
2025-07-13 21:57:24 -07:00
const mask = BigInt . asUintN (
version === 4 ? 64 : 128 ,
( BigInt ( 1 ) << shiftBits ) - BigInt ( 1 )
) ;
2024-10-26 16:04:01 -04:00
const start = ipBigInt & ~ mask ;
const end = start | mask ;
2025-05-03 12:21:43 -04:00
2024-10-26 16:04:01 -04:00
return { start , end } ;
2024-10-26 22:44:34 -04:00
}
/ * *
* Finds the next available CIDR block given existing allocations
* @param existingCidrs Array of existing CIDR blocks
2025-02-14 12:32:10 -05:00
* @param blockSize Desired prefix length for the new block
* @param startCidr Optional CIDR to start searching from
2024-10-26 22:44:34 -04:00
* @returns Next available CIDR block or null if none found
* /
export function findNextAvailableCidr (
2024-10-26 16:04:01 -04:00
existingCidrs : string [ ] ,
blockSize : number ,
2025-02-14 12:32:10 -05:00
startCidr? : string
2024-10-26 22:44:34 -04:00
) : string | null {
2025-02-14 15:49:23 -05:00
if ( ! startCidr && existingCidrs . length === 0 ) {
return null ;
}
2025-05-03 12:21:43 -04:00
2025-02-14 15:49:23 -05:00
// If no existing CIDRs, use the IP version from startCidr
2025-07-13 21:57:24 -07:00
const version = startCidr ? detectIpVersion ( startCidr . split ( "/" ) [ 0 ] ) : 4 ; // Default to IPv4 if no startCidr provided
2025-05-03 12:21:43 -04:00
2025-02-14 12:32:10 -05:00
// Use appropriate default startCidr if none provided
startCidr = startCidr || ( version === 4 ? "0.0.0.0/0" : "::/0" ) ;
2025-05-03 12:21:43 -04:00
2025-02-14 15:49:23 -05:00
// If there are existing CIDRs, ensure all are same version
2025-07-13 21:57:24 -07:00
if (
existingCidrs . length > 0 &&
existingCidrs . some (
( cidr ) = > detectIpVersion ( cidr . split ( "/" ) [ 0 ] ) !== version
)
) {
throw new Error ( "All CIDRs must be of the same IP version" ) ;
2025-02-14 12:32:10 -05:00
}
2025-07-13 21:57:24 -07:00
2025-03-27 22:13:39 -04:00
// Extract the network part from startCidr to ensure we stay in the right subnet
const startCidrRange = cidrToRange ( startCidr ) ;
2025-07-13 21:57:24 -07:00
2024-10-26 16:04:01 -04:00
// Convert existing CIDRs to ranges and sort them
const existingRanges = existingCidrs
2025-07-13 21:57:24 -07:00
. map ( ( cidr ) = > cidrToRange ( cidr ) )
2024-10-26 22:44:34 -04:00
. sort ( ( a , b ) = > ( a . start < b . start ? - 1 : 1 ) ) ;
2025-07-13 21:57:24 -07:00
2024-10-26 16:04:01 -04:00
// Calculate block size
2025-02-14 12:32:10 -05:00
const maxPrefix = version === 4 ? 32 : 128 ;
const blockSizeBigInt = BigInt ( 1 ) << BigInt ( maxPrefix - blockSize ) ;
2025-07-13 21:57:24 -07:00
2024-10-26 16:04:01 -04:00
// Start from the beginning of the given CIDR
2025-03-27 22:13:39 -04:00
let current = startCidrRange . start ;
const maxIp = startCidrRange . end ;
2025-07-13 21:57:24 -07:00
2024-10-26 16:04:01 -04:00
// Iterate through existing ranges
for ( let i = 0 ; i <= existingRanges . length ; i ++ ) {
2024-10-26 22:44:34 -04:00
const nextRange = existingRanges [ i ] ;
2025-07-13 21:57:24 -07:00
2024-10-26 22:44:34 -04:00
// Align current to block size
2025-07-13 21:57:24 -07:00
const alignedCurrent =
current +
( ( blockSizeBigInt - ( current % blockSizeBigInt ) ) % blockSizeBigInt ) ;
2024-10-26 22:44:34 -04:00
// Check if we've gone beyond the maximum allowed IP
if ( alignedCurrent + blockSizeBigInt - BigInt ( 1 ) > maxIp ) {
return null ;
}
2025-07-13 21:57:24 -07:00
2024-10-26 22:44:34 -04:00
// If we're at the end of existing ranges or found a gap
2025-07-13 21:57:24 -07:00
if (
! nextRange ||
alignedCurrent + blockSizeBigInt - BigInt ( 1 ) < nextRange . start
) {
2025-02-14 12:32:10 -05:00
return ` ${ bigIntToIp ( alignedCurrent , version ) } / ${ blockSize } ` ;
2024-10-26 22:44:34 -04:00
}
2025-07-13 21:57:24 -07:00
2025-03-27 22:13:39 -04:00
// If next range overlaps with our search space, move past it
if ( nextRange . end >= startCidrRange . start && nextRange . start <= maxIp ) {
// Move current pointer to after the current range
current = nextRange . end + BigInt ( 1 ) ;
}
2024-10-26 16:04:01 -04:00
}
2025-07-13 21:57:24 -07:00
2024-10-26 16:04:01 -04:00
return null ;
2024-10-26 22:44:34 -04:00
}
/ * *
2025-02-14 12:32:10 -05:00
* Checks if a given IP address is within a CIDR range
* @param ip IP address to check
* @param cidr CIDR range to check against
* @returns boolean indicating if IP is within the CIDR range
* /
2024-10-26 22:44:34 -04:00
export function isIpInCidr ( ip : string , cidr : string ) : boolean {
2025-02-14 12:32:10 -05:00
const ipVersion = detectIpVersion ( ip ) ;
2025-07-13 21:57:24 -07:00
const cidrVersion = detectIpVersion ( cidr . split ( "/" ) [ 0 ] ) ;
2025-05-03 12:21:43 -04:00
// If IP versions don't match, the IP cannot be in the CIDR range
2025-02-14 12:32:10 -05:00
if ( ipVersion !== cidrVersion ) {
2025-05-03 12:21:43 -04:00
// throw new Erorr
return false ;
2025-02-14 12:32:10 -05:00
}
2024-10-26 22:44:34 -04:00
const ipBigInt = ipToBigInt ( ip ) ;
const range = cidrToRange ( cidr ) ;
return ipBigInt >= range . start && ipBigInt <= range . end ;
2025-03-25 22:01:08 -04:00
}
2025-07-13 21:57:24 -07:00
export async function getNextAvailableClientSubnet (
orgId : string
) : Promise < string > {
const [ org ] = await db . select ( ) . from ( orgs ) . where ( eq ( orgs . orgId , orgId ) ) ;
2025-04-01 12:49:02 -04:00
2025-03-25 22:01:08 -04:00
const existingAddressesSites = await db
. select ( {
address : sites.address
} )
. from ( sites )
. where ( and ( isNotNull ( sites . address ) , eq ( sites . orgId , orgId ) ) ) ;
const existingAddressesClients = await db
. select ( {
address : clients.subnet
} )
. from ( clients )
. where ( and ( isNotNull ( clients . subnet ) , eq ( clients . orgId , orgId ) ) ) ;
const addresses = [
2025-07-13 21:57:24 -07:00
. . . existingAddressesSites . map (
( site ) = > ` ${ site . address ? . split ( "/" ) [ 0 ] } /32 `
) , // we are overriding the 32 so that we pick individual addresses in the subnet of the org for the site and the client even though they are stored with the /block_size of the org
. . . existingAddressesClients . map (
( client ) = > ` ${ client . address . split ( "/" ) } /32 `
)
2025-03-25 22:01:08 -04:00
] . filter ( ( address ) = > address !== null ) as string [ ] ;
2025-07-13 21:57:24 -07:00
let subnet = findNextAvailableCidr ( addresses , 32 , org . subnet ) ; // pick the sites address in the org
2025-03-25 22:01:08 -04:00
if ( ! subnet ) {
throw new Error ( "No available subnets remaining in space" ) ;
}
return subnet ;
}
export async function getNextAvailableOrgSubnet ( ) : Promise < string > {
const existingAddresses = await db
. select ( {
subnet : orgs.subnet
} )
. from ( orgs )
. where ( isNotNull ( orgs . subnet ) ) ;
const addresses = existingAddresses . map ( ( org ) = > org . subnet ) ;
let subnet = findNextAvailableCidr (
addresses ,
config . getRawConfig ( ) . orgs . block_size ,
config . getRawConfig ( ) . orgs . subnet_group
) ;
if ( ! subnet ) {
throw new Error ( "No available subnets remaining in space" ) ;
}
return subnet ;
2025-05-11 10:31:29 -04:00
}