diff --git a/src/main/java/com/litoralregas/openvpn/openvpn/CreateIpAllocationRequest.java b/src/main/java/com/litoralregas/openvpn/openvpn/CreateIpAllocationRequest.java new file mode 100644 index 0000000..00f24af --- /dev/null +++ b/src/main/java/com/litoralregas/openvpn/openvpn/CreateIpAllocationRequest.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/openvpn/openvpn/IpAllocation.java b/src/main/java/com/litoralregas/openvpn/openvpn/IpAllocation.java new file mode 100644 index 0000000..479e9f3 --- /dev/null +++ b/src/main/java/com/litoralregas/openvpn/openvpn/IpAllocation.java @@ -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 +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/openvpn/openvpn/IpAllocationController.java b/src/main/java/com/litoralregas/openvpn/openvpn/IpAllocationController.java new file mode 100644 index 0000000..2cb2e30 --- /dev/null +++ b/src/main/java/com/litoralregas/openvpn/openvpn/IpAllocationController.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/openvpn/openvpn/IpAllocationMode.java b/src/main/java/com/litoralregas/openvpn/openvpn/IpAllocationMode.java new file mode 100644 index 0000000..07d9f35 --- /dev/null +++ b/src/main/java/com/litoralregas/openvpn/openvpn/IpAllocationMode.java @@ -0,0 +1,6 @@ +package com.litoralregas.openvpn.openvpn; + +public enum IpAllocationMode { + AUTOMATIC, + MANUAL +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/openvpn/openvpn/IpAllocationRepository.java b/src/main/java/com/litoralregas/openvpn/openvpn/IpAllocationRepository.java new file mode 100644 index 0000000..35d64c6 --- /dev/null +++ b/src/main/java/com/litoralregas/openvpn/openvpn/IpAllocationRepository.java @@ -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 { + + boolean existsByClientName(String clientName); + + boolean existsByLanSubnet(String lanSubnet); + + boolean existsByVpnIp(String vpnIp); +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/openvpn/openvpn/IpAllocationResponse.java b/src/main/java/com/litoralregas/openvpn/openvpn/IpAllocationResponse.java new file mode 100644 index 0000000..4b4b8c6 --- /dev/null +++ b/src/main/java/com/litoralregas/openvpn/openvpn/IpAllocationResponse.java @@ -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() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/openvpn/openvpn/IpAllocationService.java b/src/main/java/com/litoralregas/openvpn/openvpn/IpAllocationService.java new file mode 100644 index 0000000..64da794 --- /dev/null +++ b/src/main/java/com/litoralregas/openvpn/openvpn/IpAllocationService.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/openvpn/ssh/SshTestController.java b/src/main/java/com/litoralregas/openvpn/ssh/SshTestController.java deleted file mode 100644 index 54496ae..0000000 --- a/src/main/java/com/litoralregas/openvpn/ssh/SshTestController.java +++ /dev/null @@ -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" - ); - } -} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/openvpn/ssh/SshTestRequest.java b/src/main/java/com/litoralregas/openvpn/ssh/SshTestRequest.java deleted file mode 100644 index 5d1e605..0000000 --- a/src/main/java/com/litoralregas/openvpn/ssh/SshTestRequest.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.litoralregas.openvpn.ssh; - -public record SshTestRequest( - String host, - int port, - String username, - String password -) { -} \ No newline at end of file diff --git a/src/main/resources/db/migration/V5__ip_allocations_table.sql b/src/main/resources/db/migration/V5__ip_allocations_table.sql new file mode 100644 index 0000000..72ee025 --- /dev/null +++ b/src/main/resources/db/migration/V5__ip_allocations_table.sql @@ -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) +); \ No newline at end of file