Use VPS as source of truth for VPN IP allocation
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
package com.litoralregas.vpnprovisioner.ipam.dto;
|
||||
|
||||
import com.litoralregas.vpnprovisioner.ipam.entity.VpnIpAllocationStatus;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
public record VpnIpAllocationResponse(
|
||||
UUID id,
|
||||
String ipAddress,
|
||||
boolean allocated,
|
||||
VpnIpAllocationStatus status,
|
||||
UUID routerId,
|
||||
Instant allocatedAt,
|
||||
Instant releasedAt
|
||||
|
||||
@@ -15,8 +15,9 @@ public class VpnIpAllocation {
|
||||
@Column(nullable = false, unique = true)
|
||||
private String ipAddress;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private boolean allocated;
|
||||
private VpnIpAllocationStatus status;
|
||||
|
||||
private UUID routerId;
|
||||
|
||||
@@ -30,18 +31,32 @@ public class VpnIpAllocation {
|
||||
public VpnIpAllocation(String ipAddress) {
|
||||
this.id = UUID.randomUUID();
|
||||
this.ipAddress = ipAddress;
|
||||
this.allocated = false;
|
||||
this.status = VpnIpAllocationStatus.ALLOCATED_EXTERNAL;
|
||||
this.allocatedAt = Instant.now();
|
||||
}
|
||||
|
||||
public static VpnIpAllocation backendAllocated(String ipAddress, UUID routerId) {
|
||||
VpnIpAllocation allocation = new VpnIpAllocation(ipAddress);
|
||||
allocation.allocateTo(routerId);
|
||||
return allocation;
|
||||
}
|
||||
|
||||
public void allocateTo(UUID routerId) {
|
||||
this.allocated = true;
|
||||
this.status = VpnIpAllocationStatus.ALLOCATED;
|
||||
this.routerId = routerId;
|
||||
this.allocatedAt = Instant.now();
|
||||
this.releasedAt = null;
|
||||
}
|
||||
|
||||
public void markExternal() {
|
||||
if (this.status != VpnIpAllocationStatus.ALLOCATED) {
|
||||
this.status = VpnIpAllocationStatus.ALLOCATED_EXTERNAL;
|
||||
this.allocatedAt = this.allocatedAt == null ? Instant.now() : this.allocatedAt;
|
||||
}
|
||||
}
|
||||
|
||||
public void release() {
|
||||
this.allocated = false;
|
||||
this.status = VpnIpAllocationStatus.RELEASED;
|
||||
this.routerId = null;
|
||||
this.releasedAt = Instant.now();
|
||||
}
|
||||
@@ -54,8 +69,8 @@ public class VpnIpAllocation {
|
||||
return ipAddress;
|
||||
}
|
||||
|
||||
public boolean isAllocated() {
|
||||
return allocated;
|
||||
public VpnIpAllocationStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public UUID getRouterId() {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.litoralregas.vpnprovisioner.ipam.entity;
|
||||
|
||||
public enum VpnIpAllocationStatus {
|
||||
ALLOCATED,
|
||||
ALLOCATED_EXTERNAL,
|
||||
RELEASED,
|
||||
RETIRED
|
||||
}
|
||||
+2
-28
@@ -4,7 +4,6 @@ import com.litoralregas.vpnprovisioner.ipam.entity.VpnIpAllocation;
|
||||
import org.springframework.data.jpa.repository.*;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -12,40 +11,15 @@ public interface VpnIpAllocationRepository extends JpaRepository<VpnIpAllocation
|
||||
|
||||
Optional<VpnIpAllocation> findByRouterId(UUID routerId);
|
||||
|
||||
@Query(
|
||||
value = """
|
||||
SELECT *
|
||||
FROM vpn_ip_allocations
|
||||
WHERE allocated = false
|
||||
ORDER BY ip_address
|
||||
LIMIT 1
|
||||
FOR UPDATE SKIP LOCKED
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
Optional<VpnIpAllocation> findNextFreeForUpdate();
|
||||
Optional<VpnIpAllocation> findByIpAddress(String ipAddress);
|
||||
|
||||
@Modifying
|
||||
@Query("""
|
||||
UPDATE VpnIpAllocation ip
|
||||
SET ip.allocated = false,
|
||||
SET ip.status = 'RELEASED',
|
||||
ip.routerId = null,
|
||||
ip.releasedAt = CURRENT_TIMESTAMP
|
||||
WHERE ip.routerId = :routerId
|
||||
""")
|
||||
void releaseByRouterId(@Param("routerId") UUID routerId);
|
||||
|
||||
@Query(
|
||||
value = """
|
||||
SELECT *
|
||||
FROM vpn_ip_allocations
|
||||
WHERE allocated = false
|
||||
ORDER BY ip_address
|
||||
FOR UPDATE SKIP LOCKED
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
List<VpnIpAllocation> findFreeCandidatesForUpdate();
|
||||
|
||||
Optional<VpnIpAllocation> findByIpAddress(String ipAddress);
|
||||
}
|
||||
+49
-11
@@ -10,13 +10,18 @@ import com.litoralregas.vpnprovisioner.vps.WireGuardVpsService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class VpnIpAllocationService {
|
||||
|
||||
private static final int START_THIRD_OCTET = 1;
|
||||
private static final int END_THIRD_OCTET = 255;
|
||||
private static final int START_FOURTH_OCTET = 1;
|
||||
private static final int END_FOURTH_OCTET = 254;
|
||||
|
||||
private final VpnIpAllocationRepository vpnIpAllocationRepository;
|
||||
private final RouterRepository routerRepository;
|
||||
private final WireGuardVpsService wireGuardVpsService;
|
||||
@@ -24,7 +29,8 @@ public class VpnIpAllocationService {
|
||||
public VpnIpAllocationService(
|
||||
VpnIpAllocationRepository vpnIpAllocationRepository,
|
||||
RouterRepository routerRepository,
|
||||
WireGuardVpsService wireGuardVpsService) {
|
||||
WireGuardVpsService wireGuardVpsService
|
||||
) {
|
||||
this.vpnIpAllocationRepository = vpnIpAllocationRepository;
|
||||
this.routerRepository = routerRepository;
|
||||
this.wireGuardVpsService = wireGuardVpsService;
|
||||
@@ -46,25 +52,57 @@ public class VpnIpAllocationService {
|
||||
}
|
||||
|
||||
private VpnIpAllocationResponse allocateNewIp(Router router) {
|
||||
Set<String> usedOnVps = refreshUsedIpsFromVps();
|
||||
|
||||
Set<String> knownInDb = vpnIpAllocationRepository.findAll()
|
||||
.stream()
|
||||
.map(VpnIpAllocation::getIpAddress)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
String nextIp = findNextAvailableIp(usedOnVps, knownInDb);
|
||||
|
||||
VpnIpAllocation allocation = VpnIpAllocation.backendAllocated(nextIp, router.getId());
|
||||
|
||||
VpnIpAllocation saved = vpnIpAllocationRepository.save(allocation);
|
||||
|
||||
router.assignVpnIp(saved.getIpAddress());
|
||||
|
||||
return toResponse(saved);
|
||||
}
|
||||
|
||||
private Set<String> refreshUsedIpsFromVps() {
|
||||
Set<String> usedIps = wireGuardVpsService.findUsedVpnIps();
|
||||
|
||||
VpnIpAllocation allocation = vpnIpAllocationRepository
|
||||
.findFreeCandidatesForUpdate()
|
||||
.stream()
|
||||
.filter(ip -> !usedIps.contains(ip.getIpAddress()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("No free VPN IP addresses available"));
|
||||
for (String ip : usedIps) {
|
||||
vpnIpAllocationRepository.findByIpAddress(ip)
|
||||
.ifPresentOrElse(
|
||||
VpnIpAllocation::markExternal,
|
||||
() -> vpnIpAllocationRepository.save(new VpnIpAllocation(ip))
|
||||
);
|
||||
}
|
||||
|
||||
allocation.allocateTo(router.getId());
|
||||
return usedIps;
|
||||
}
|
||||
|
||||
return toResponse(allocation);
|
||||
private String findNextAvailableIp(Set<String> usedOnVps, Set<String> knownInDb) {
|
||||
for (int thirdOctet = START_THIRD_OCTET; thirdOctet <= END_THIRD_OCTET; thirdOctet++) {
|
||||
for (int fourthOctet = START_FOURTH_OCTET; fourthOctet <= END_FOURTH_OCTET; fourthOctet++) {
|
||||
String ip = "198.19." + thirdOctet + "." + fourthOctet;
|
||||
|
||||
if (!usedOnVps.contains(ip) && !knownInDb.contains(ip)) {
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalStateException("No free VPN IP addresses available");
|
||||
}
|
||||
|
||||
private VpnIpAllocationResponse toResponse(VpnIpAllocation allocation) {
|
||||
return new VpnIpAllocationResponse(
|
||||
allocation.getId(),
|
||||
allocation.getIpAddress(),
|
||||
allocation.isAllocated(),
|
||||
allocation.getStatus(),
|
||||
allocation.getRouterId(),
|
||||
allocation.getAllocatedAt(),
|
||||
allocation.getReleasedAt()
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.litoralregas.vpnprovisioner.ipam.service;
|
||||
|
||||
import com.litoralregas.vpnprovisioner.ipam.entity.VpnIpAllocation;
|
||||
import com.litoralregas.vpnprovisioner.ipam.repository.VpnIpAllocationRepository;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class VpnIpPoolSeeder implements CommandLineRunner {
|
||||
|
||||
private final VpnIpAllocationRepository repository;
|
||||
|
||||
public VpnIpPoolSeeder(VpnIpAllocationRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(String... args) {
|
||||
|
||||
for (int thirdOctet = 1; thirdOctet <= 1; thirdOctet++) {
|
||||
|
||||
for (int fourthOctet = 2; fourthOctet <= 254; fourthOctet++) {
|
||||
|
||||
String ip = "198.19." + thirdOctet + "." + fourthOctet;
|
||||
|
||||
if (repository.findByIpAddress(ip).isEmpty()) {
|
||||
repository.save(new VpnIpAllocation(ip));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,4 +63,8 @@ public class Router {
|
||||
this.hardwareModel = hardwareModel;
|
||||
this.firmwareVersion = firmwareVersion;
|
||||
}
|
||||
|
||||
public void assignVpnIp(String vpnIp) {
|
||||
this.vpnIp = vpnIp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
ALTER TABLE vpn_ip_allocations
|
||||
ADD COLUMN status VARCHAR(50);
|
||||
|
||||
UPDATE vpn_ip_allocations
|
||||
SET status = CASE
|
||||
WHEN allocated = true THEN 'ALLOCATED'
|
||||
ELSE 'AVAILABLE'
|
||||
END;
|
||||
|
||||
ALTER TABLE vpn_ip_allocations
|
||||
ALTER COLUMN status SET NOT NULL;
|
||||
Reference in New Issue
Block a user