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;
|
package com.litoralregas.vpnprovisioner.ipam.dto;
|
||||||
|
|
||||||
|
import com.litoralregas.vpnprovisioner.ipam.entity.VpnIpAllocationStatus;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public record VpnIpAllocationResponse(
|
public record VpnIpAllocationResponse(
|
||||||
UUID id,
|
UUID id,
|
||||||
String ipAddress,
|
String ipAddress,
|
||||||
boolean allocated,
|
VpnIpAllocationStatus status,
|
||||||
UUID routerId,
|
UUID routerId,
|
||||||
Instant allocatedAt,
|
Instant allocatedAt,
|
||||||
Instant releasedAt
|
Instant releasedAt
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ public class VpnIpAllocation {
|
|||||||
@Column(nullable = false, unique = true)
|
@Column(nullable = false, unique = true)
|
||||||
private String ipAddress;
|
private String ipAddress;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private boolean allocated;
|
private VpnIpAllocationStatus status;
|
||||||
|
|
||||||
private UUID routerId;
|
private UUID routerId;
|
||||||
|
|
||||||
@@ -30,18 +31,32 @@ public class VpnIpAllocation {
|
|||||||
public VpnIpAllocation(String ipAddress) {
|
public VpnIpAllocation(String ipAddress) {
|
||||||
this.id = UUID.randomUUID();
|
this.id = UUID.randomUUID();
|
||||||
this.ipAddress = ipAddress;
|
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) {
|
public void allocateTo(UUID routerId) {
|
||||||
this.allocated = true;
|
this.status = VpnIpAllocationStatus.ALLOCATED;
|
||||||
this.routerId = routerId;
|
this.routerId = routerId;
|
||||||
this.allocatedAt = Instant.now();
|
this.allocatedAt = Instant.now();
|
||||||
this.releasedAt = null;
|
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() {
|
public void release() {
|
||||||
this.allocated = false;
|
this.status = VpnIpAllocationStatus.RELEASED;
|
||||||
this.routerId = null;
|
this.routerId = null;
|
||||||
this.releasedAt = Instant.now();
|
this.releasedAt = Instant.now();
|
||||||
}
|
}
|
||||||
@@ -54,8 +69,8 @@ public class VpnIpAllocation {
|
|||||||
return ipAddress;
|
return ipAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAllocated() {
|
public VpnIpAllocationStatus getStatus() {
|
||||||
return allocated;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
public UUID getRouterId() {
|
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.jpa.repository.*;
|
||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -12,40 +11,15 @@ public interface VpnIpAllocationRepository extends JpaRepository<VpnIpAllocation
|
|||||||
|
|
||||||
Optional<VpnIpAllocation> findByRouterId(UUID routerId);
|
Optional<VpnIpAllocation> findByRouterId(UUID routerId);
|
||||||
|
|
||||||
@Query(
|
Optional<VpnIpAllocation> findByIpAddress(String ipAddress);
|
||||||
value = """
|
|
||||||
SELECT *
|
|
||||||
FROM vpn_ip_allocations
|
|
||||||
WHERE allocated = false
|
|
||||||
ORDER BY ip_address
|
|
||||||
LIMIT 1
|
|
||||||
FOR UPDATE SKIP LOCKED
|
|
||||||
""",
|
|
||||||
nativeQuery = true
|
|
||||||
)
|
|
||||||
Optional<VpnIpAllocation> findNextFreeForUpdate();
|
|
||||||
|
|
||||||
@Modifying
|
@Modifying
|
||||||
@Query("""
|
@Query("""
|
||||||
UPDATE VpnIpAllocation ip
|
UPDATE VpnIpAllocation ip
|
||||||
SET ip.allocated = false,
|
SET ip.status = 'RELEASED',
|
||||||
ip.routerId = null,
|
ip.routerId = null,
|
||||||
ip.releasedAt = CURRENT_TIMESTAMP
|
ip.releasedAt = CURRENT_TIMESTAMP
|
||||||
WHERE ip.routerId = :routerId
|
WHERE ip.routerId = :routerId
|
||||||
""")
|
""")
|
||||||
void releaseByRouterId(@Param("routerId") UUID 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.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class VpnIpAllocationService {
|
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 VpnIpAllocationRepository vpnIpAllocationRepository;
|
||||||
private final RouterRepository routerRepository;
|
private final RouterRepository routerRepository;
|
||||||
private final WireGuardVpsService wireGuardVpsService;
|
private final WireGuardVpsService wireGuardVpsService;
|
||||||
@@ -24,7 +29,8 @@ public class VpnIpAllocationService {
|
|||||||
public VpnIpAllocationService(
|
public VpnIpAllocationService(
|
||||||
VpnIpAllocationRepository vpnIpAllocationRepository,
|
VpnIpAllocationRepository vpnIpAllocationRepository,
|
||||||
RouterRepository routerRepository,
|
RouterRepository routerRepository,
|
||||||
WireGuardVpsService wireGuardVpsService) {
|
WireGuardVpsService wireGuardVpsService
|
||||||
|
) {
|
||||||
this.vpnIpAllocationRepository = vpnIpAllocationRepository;
|
this.vpnIpAllocationRepository = vpnIpAllocationRepository;
|
||||||
this.routerRepository = routerRepository;
|
this.routerRepository = routerRepository;
|
||||||
this.wireGuardVpsService = wireGuardVpsService;
|
this.wireGuardVpsService = wireGuardVpsService;
|
||||||
@@ -46,25 +52,57 @@ public class VpnIpAllocationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private VpnIpAllocationResponse allocateNewIp(Router router) {
|
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();
|
Set<String> usedIps = wireGuardVpsService.findUsedVpnIps();
|
||||||
|
|
||||||
VpnIpAllocation allocation = vpnIpAllocationRepository
|
for (String ip : usedIps) {
|
||||||
.findFreeCandidatesForUpdate()
|
vpnIpAllocationRepository.findByIpAddress(ip)
|
||||||
.stream()
|
.ifPresentOrElse(
|
||||||
.filter(ip -> !usedIps.contains(ip.getIpAddress()))
|
VpnIpAllocation::markExternal,
|
||||||
.findFirst()
|
() -> vpnIpAllocationRepository.save(new VpnIpAllocation(ip))
|
||||||
.orElseThrow(() -> new IllegalStateException("No free VPN IP addresses available"));
|
);
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
private VpnIpAllocationResponse toResponse(VpnIpAllocation allocation) {
|
||||||
return new VpnIpAllocationResponse(
|
return new VpnIpAllocationResponse(
|
||||||
allocation.getId(),
|
allocation.getId(),
|
||||||
allocation.getIpAddress(),
|
allocation.getIpAddress(),
|
||||||
allocation.isAllocated(),
|
allocation.getStatus(),
|
||||||
allocation.getRouterId(),
|
allocation.getRouterId(),
|
||||||
allocation.getAllocatedAt(),
|
allocation.getAllocatedAt(),
|
||||||
allocation.getReleasedAt()
|
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.hardwareModel = hardwareModel;
|
||||||
this.firmwareVersion = firmwareVersion;
|
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