Validate IP allocation against live OpenVPN clients

This commit is contained in:
litoral05
2026-05-05 12:21:28 +01:00
parent 2b8aa685b0
commit c09aff2fcb
10 changed files with 344 additions and 30 deletions
@@ -0,0 +1,39 @@
package com.litoralregas.openvpn.openvpn;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
public class CreateIpAllocationRequest {
@NotBlank
private String clientName;
@NotNull
private IpAllocationMode allocationMode;
private Integer number;
public String getClientName() {
return clientName;
}
public IpAllocationMode getAllocationMode() {
return allocationMode;
}
public Integer getNumber() {
return number;
}
public void setClientName(String clientName) {
this.clientName = clientName;
}
public void setAllocationMode(IpAllocationMode allocationMode) {
this.allocationMode = allocationMode;
}
public void setNumber(Integer number) {
this.number = number;
}
}
@@ -0,0 +1,89 @@
package com.litoralregas.openvpn.openvpn;
import com.litoralregas.openvpn.router.Router;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.UUID;
@Entity
@Table(name = "ip_allocations")
public class IpAllocation {
@Id
private UUID id;
@ManyToOne
@JoinColumn(name = "router_id", nullable = false)
private Router router;
private String clientName;
private String lanSubnet;
private String vpnIp;
@Enumerated(EnumType.STRING)
private IpAllocationMode allocationMode;
private LocalDateTime createdAt;
public IpAllocation() {
}
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public Router getRouter() {
return router;
}
public void setRouter(Router router) {
this.router = router;
}
public String getClientName() {
return clientName;
}
public void setClientName(String clientName) {
this.clientName = clientName;
}
public String getLanSubnet() {
return lanSubnet;
}
public void setLanSubnet(String lanSubnet) {
this.lanSubnet = lanSubnet;
}
public String getVpnIp() {
return vpnIp;
}
public void setVpnIp(String vpnIp) {
this.vpnIp = vpnIp;
}
public IpAllocationMode getAllocationMode() {
return allocationMode;
}
public void setAllocationMode(IpAllocationMode allocationMode) {
this.allocationMode = allocationMode;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
// getters and setters
}
@@ -0,0 +1,30 @@
package com.litoralregas.openvpn.openvpn;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import java.util.UUID;
@RestController
@RequestMapping("/api/routers/{routerId}/ip-allocation")
public class IpAllocationController {
private final IpAllocationService service;
public IpAllocationController(IpAllocationService service) {
this.service = service;
}
@PostMapping
public IpAllocationResponse allocate(
@PathVariable UUID routerId,
@Valid @RequestBody CreateIpAllocationRequest request
) {
return service.allocate(routerId, request);
}
@DeleteMapping("/{allocationId}")
public void delete(@PathVariable UUID allocationId) {
service.delete(allocationId);
}
}
@@ -0,0 +1,6 @@
package com.litoralregas.openvpn.openvpn;
public enum IpAllocationMode {
AUTOMATIC,
MANUAL
}
@@ -0,0 +1,14 @@
package com.litoralregas.openvpn.openvpn;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;
public interface IpAllocationRepository extends JpaRepository<IpAllocation, UUID> {
boolean existsByClientName(String clientName);
boolean existsByLanSubnet(String lanSubnet);
boolean existsByVpnIp(String vpnIp);
}
@@ -0,0 +1,28 @@
package com.litoralregas.openvpn.openvpn;
import java.time.LocalDateTime;
import java.util.UUID;
public record IpAllocationResponse(
UUID id,
UUID routerId,
String routerName,
String clientName,
String lanSubnet,
String vpnIp,
IpAllocationMode allocationMode,
LocalDateTime createdAt
) {
public static IpAllocationResponse from(IpAllocation allocation) {
return new IpAllocationResponse(
allocation.getId(),
allocation.getRouter().getId(),
allocation.getRouter().getName(),
allocation.getClientName(),
allocation.getLanSubnet(),
allocation.getVpnIp(),
allocation.getAllocationMode(),
allocation.getCreatedAt()
);
}
}
@@ -0,0 +1,116 @@
package com.litoralregas.openvpn.openvpn;
import com.litoralregas.openvpn.router.Router;
import com.litoralregas.openvpn.router.RouterService;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.UUID;
@Service
public class IpAllocationService {
private final IpAllocationRepository repository;
private final RouterService routerService;
private final OpenVpnService openVpnService;
public IpAllocationService(
IpAllocationRepository repository,
RouterService routerService,
OpenVpnService openVpnService) {
this.repository = repository;
this.routerService = routerService;
this.openVpnService = openVpnService;
}
public IpAllocationResponse allocate(UUID routerId, CreateIpAllocationRequest request) {
Router router = routerService.findById(routerId);
if (repository.existsByClientName(request.getClientName())) {
throw new IllegalArgumentException("Client name already allocated: " + request.getClientName());
}
int number = resolveNumber(request);
String lanSubnet = "192.168." + number + ".0";
String vpnIp = "198.20.1." + number;
validateNumber(number);
validateNoDuplicates(lanSubnet, vpnIp);
validateAgainstLiveClients(
request.getClientName(),
vpnIp,
lanSubnet
);
IpAllocation allocation = new IpAllocation();
allocation.setId(UUID.randomUUID());
allocation.setRouter(router);
allocation.setClientName(request.getClientName());
allocation.setLanSubnet(lanSubnet);
allocation.setVpnIp(vpnIp);
allocation.setAllocationMode(request.getAllocationMode());
allocation.setCreatedAt(LocalDateTime.now());
return IpAllocationResponse.from(repository.save(allocation));
}
private int resolveNumber(CreateIpAllocationRequest request) {
if (request.getAllocationMode() == IpAllocationMode.MANUAL) {
if (request.getNumber() == null) {
throw new IllegalArgumentException("Manual allocation requires a number");
}
return request.getNumber();
}
for (int number = 2; number <= 254; number++) {
String lanSubnet = "192.168." + number + ".0";
String vpnIp = "198.20.1." + number;
if (!repository.existsByLanSubnet(lanSubnet) && !repository.existsByVpnIp(vpnIp)) {
return number;
}
}
throw new IllegalArgumentException("No available IP allocations");
}
private void validateNumber(int number) {
if (number < 2 || number > 254) {
throw new IllegalArgumentException("Allocation number must be between 2 and 254");
}
}
private void validateNoDuplicates(String lanSubnet, String vpnIp) {
if (repository.existsByLanSubnet(lanSubnet)) {
throw new IllegalArgumentException("LAN subnet already allocated: " + lanSubnet);
}
if (repository.existsByVpnIp(vpnIp)) {
throw new IllegalArgumentException("VPN IP already allocated: " + vpnIp);
}
}
private void validateAgainstLiveClients(String clientName, String vpnIp, String lanSubnet) {
var clients = openVpnService.listClients();
for (var client : clients) {
if (client.clientName().equalsIgnoreCase(clientName)) {
throw new IllegalArgumentException("Client already exists on VPS: " + clientName);
}
if (client.vpnIp().equals(vpnIp)) {
throw new IllegalArgumentException("VPN IP already in use on VPS: " + vpnIp);
}
if (client.lanSubnet().equals(lanSubnet)) {
throw new IllegalArgumentException("LAN subnet already in use on VPS: " + lanSubnet);
}
}
}
public void delete(UUID id) {
repository.deleteById(id);
}
}
@@ -1,21 +0,0 @@
package com.litoralregas.openvpn.ssh;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/ssh-test")
public class SshTestController {
private final SshService sshService;
public SshTestController(SshService sshService) {
this.sshService = sshService;
}
@PostMapping
public SshCommandResult test() {
return sshService.executeOnConfiguredVps(
"/var/litoral_regas_openvpn/tools/list-clients.sh"
);
}
}
@@ -1,9 +0,0 @@
package com.litoralregas.openvpn.ssh;
public record SshTestRequest(
String host,
int port,
String username,
String password
) {
}
@@ -0,0 +1,22 @@
CREATE TABLE ip_allocations (
id UUID PRIMARY KEY,
router_id UUID NOT NULL,
client_name VARCHAR(120) NOT NULL,
lan_subnet VARCHAR(50) NOT NULL,
vpn_ip VARCHAR(50) NOT NULL,
allocation_mode VARCHAR(40) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT now(),
CONSTRAINT fk_ip_allocations_router
FOREIGN KEY (router_id)
REFERENCES routers (id)
ON DELETE CASCADE,
CONSTRAINT uk_ip_allocations_lan_subnet UNIQUE (lan_subnet),
CONSTRAINT uk_ip_allocations_vpn_ip UNIQUE (vpn_ip),
CONSTRAINT uk_ip_allocations_client_name UNIQUE (client_name)
);