Move VPN provisioning onto router model
This commit is contained in:
+13
-1
@@ -1,9 +1,11 @@
|
||||
package com.litoralregas.vpnprovisioner.router.controller;
|
||||
|
||||
import com.litoralregas.vpnprovisioner.router.dto.CreateRouterRequest;
|
||||
import com.litoralregas.vpnprovisioner.router.dto.ProvisionRouterVpnRequest;
|
||||
import com.litoralregas.vpnprovisioner.router.dto.RouterResponse;
|
||||
import com.litoralregas.vpnprovisioner.router.dto.UpdateRouterRequest;
|
||||
import com.litoralregas.vpnprovisioner.router.service.RouterService;
|
||||
import com.litoralregas.vpnprovisioner.router.service.RouterVpnProvisioningService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -16,9 +18,11 @@ import java.util.UUID;
|
||||
public class RouterController {
|
||||
|
||||
private final RouterService routerService;
|
||||
private final RouterVpnProvisioningService routerVpnProvisioningService;
|
||||
|
||||
public RouterController(RouterService routerService) {
|
||||
public RouterController(RouterService routerService, RouterVpnProvisioningService routerVpnProvisioningService) {
|
||||
this.routerService = routerService;
|
||||
this.routerVpnProvisioningService = routerVpnProvisioningService;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@@ -50,4 +54,12 @@ public class RouterController {
|
||||
public void delete(@PathVariable UUID id) {
|
||||
routerService.delete(id);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/vpn-peer")
|
||||
public RouterResponse provisionVpnPeer(
|
||||
@PathVariable UUID id,
|
||||
@Valid @RequestBody ProvisionRouterVpnRequest request
|
||||
) {
|
||||
return routerVpnProvisioningService.provision(id, request);
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.litoralregas.vpnprovisioner.router.dto;
|
||||
|
||||
import com.litoralregas.vpnprovisioner.router.entity.EndpointMode;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public record ProvisionRouterVpnRequest(
|
||||
|
||||
@NotBlank
|
||||
String publicKey,
|
||||
|
||||
@NotNull
|
||||
EndpointMode endpointMode
|
||||
) {
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.litoralregas.vpnprovisioner.router.dto;
|
||||
|
||||
import com.litoralregas.vpnprovisioner.router.entity.EndpointMode;
|
||||
import com.litoralregas.vpnprovisioner.router.entity.RouterStatus;
|
||||
import com.litoralregas.vpnprovisioner.router.entity.RouterVpnStatus;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
@@ -12,6 +14,10 @@ public record RouterResponse(
|
||||
String firmwareVersion,
|
||||
RouterStatus status,
|
||||
String vpnIp,
|
||||
String wireguardPublicKey,
|
||||
EndpointMode endpointMode,
|
||||
RouterVpnStatus vpnStatus,
|
||||
Instant vpnProvisionedAt,
|
||||
Instant createdAt,
|
||||
Instant updatedAt
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.litoralregas.vpnprovisioner.router.entity;
|
||||
|
||||
public enum EndpointMode {
|
||||
NORMAL_WIREGUARD,
|
||||
UDP2RAW
|
||||
}
|
||||
@@ -25,6 +25,17 @@ public class Router {
|
||||
|
||||
private String vpnIp;
|
||||
|
||||
@Column(unique = true)
|
||||
private String wireguardPublicKey;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
private EndpointMode endpointMode;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
private RouterVpnStatus vpnStatus;
|
||||
|
||||
private Instant vpnProvisionedAt;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
@@ -40,6 +51,7 @@ public class Router {
|
||||
this.hardwareModel = hardwareModel;
|
||||
this.firmwareVersion = firmwareVersion;
|
||||
this.status = RouterStatus.PENDING;
|
||||
this.vpnStatus = RouterVpnStatus.NOT_PROVISIONED;
|
||||
this.createdAt = Instant.now();
|
||||
this.updatedAt = Instant.now();
|
||||
}
|
||||
@@ -49,15 +61,6 @@ public class Router {
|
||||
this.updatedAt = Instant.now();
|
||||
}
|
||||
|
||||
public UUID getId() { return id; }
|
||||
public String getName() { return name; }
|
||||
public String getHardwareModel() { return hardwareModel; }
|
||||
public String getFirmwareVersion() { return firmwareVersion; }
|
||||
public RouterStatus getStatus() { return status; }
|
||||
public String getVpnIp() { return vpnIp; }
|
||||
public Instant getCreatedAt() { return createdAt; }
|
||||
public Instant getUpdatedAt() { return updatedAt; }
|
||||
|
||||
public void updateDetails(String name, String hardwareModel, String firmwareVersion) {
|
||||
this.name = name;
|
||||
this.hardwareModel = hardwareModel;
|
||||
@@ -67,4 +70,72 @@ public class Router {
|
||||
public void assignVpnIp(String vpnIp) {
|
||||
this.vpnIp = vpnIp;
|
||||
}
|
||||
|
||||
public void markVpnApplying(String publicKey, String vpnIp, EndpointMode endpointMode) {
|
||||
this.wireguardPublicKey = publicKey;
|
||||
this.vpnIp = vpnIp;
|
||||
this.endpointMode = endpointMode;
|
||||
this.vpnStatus = RouterVpnStatus.APPLYING;
|
||||
}
|
||||
|
||||
public void markVpnApplied() {
|
||||
this.vpnStatus = RouterVpnStatus.APPLIED;
|
||||
this.vpnProvisionedAt = Instant.now();
|
||||
}
|
||||
|
||||
public void markVpnFailed() {
|
||||
this.vpnStatus = RouterVpnStatus.FAILED;
|
||||
}
|
||||
|
||||
public boolean hasVpnPeer() {
|
||||
return this.wireguardPublicKey != null && this.vpnIp != null;
|
||||
}
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getHardwareModel() {
|
||||
return hardwareModel;
|
||||
}
|
||||
|
||||
public String getFirmwareVersion() {
|
||||
return firmwareVersion;
|
||||
}
|
||||
|
||||
public RouterStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public String getVpnIp() {
|
||||
return vpnIp;
|
||||
}
|
||||
|
||||
public String getWireguardPublicKey() {
|
||||
return wireguardPublicKey;
|
||||
}
|
||||
|
||||
public EndpointMode getEndpointMode() {
|
||||
return endpointMode;
|
||||
}
|
||||
|
||||
public RouterVpnStatus getVpnStatus() {
|
||||
return vpnStatus;
|
||||
}
|
||||
|
||||
public Instant getVpnProvisionedAt() {
|
||||
return vpnProvisionedAt;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public Instant getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.litoralregas.vpnprovisioner.router.entity;
|
||||
|
||||
public enum RouterVpnStatus {
|
||||
NOT_PROVISIONED,
|
||||
APPLYING,
|
||||
APPLIED,
|
||||
FAILED
|
||||
}
|
||||
@@ -79,6 +79,10 @@ public class RouterService {
|
||||
router.getFirmwareVersion(),
|
||||
router.getStatus(),
|
||||
router.getVpnIp(),
|
||||
router.getWireguardPublicKey(),
|
||||
router.getEndpointMode(),
|
||||
router.getVpnStatus(),
|
||||
router.getVpnProvisionedAt(),
|
||||
router.getCreatedAt(),
|
||||
router.getUpdatedAt()
|
||||
);
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package com.litoralregas.vpnprovisioner.router.service;
|
||||
|
||||
import com.litoralregas.vpnprovisioner.common.exception.ResourceNotFoundException;
|
||||
import com.litoralregas.vpnprovisioner.router.dto.ProvisionRouterVpnRequest;
|
||||
import com.litoralregas.vpnprovisioner.router.dto.RouterResponse;
|
||||
import com.litoralregas.vpnprovisioner.router.entity.Router;
|
||||
import com.litoralregas.vpnprovisioner.router.repository.RouterRepository;
|
||||
import com.litoralregas.vpnprovisioner.vps.WireGuardVpsService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class RouterVpnProvisioningService {
|
||||
|
||||
private final RouterRepository routerRepository;
|
||||
private final WireGuardVpsService wireGuardVpsService;
|
||||
|
||||
public RouterVpnProvisioningService(
|
||||
RouterRepository routerRepository,
|
||||
WireGuardVpsService wireGuardVpsService
|
||||
) {
|
||||
this.routerRepository = routerRepository;
|
||||
this.wireGuardVpsService = wireGuardVpsService;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public RouterResponse provision(UUID routerId, ProvisionRouterVpnRequest request) {
|
||||
|
||||
Router router = routerRepository.findById(routerId)
|
||||
.orElseThrow(() ->
|
||||
new ResourceNotFoundException("Router not found: " + routerId)
|
||||
);
|
||||
|
||||
if (router.hasVpnPeer()) {
|
||||
throw new IllegalStateException("Router already has VPN provisioned");
|
||||
}
|
||||
|
||||
Set<String> usedIps = wireGuardVpsService.findUsedVpnIps();
|
||||
|
||||
String vpnIp = findNextAvailableIp(usedIps);
|
||||
|
||||
router.markVpnApplying(
|
||||
request.publicKey(),
|
||||
vpnIp,
|
||||
request.endpointMode()
|
||||
);
|
||||
|
||||
try {
|
||||
|
||||
wireGuardVpsService.applyPeer(
|
||||
request.publicKey(),
|
||||
vpnIp + "/32"
|
||||
);
|
||||
|
||||
router.markVpnApplied();
|
||||
|
||||
} catch (RuntimeException exception) {
|
||||
|
||||
router.markVpnFailed();
|
||||
|
||||
throw exception;
|
||||
}
|
||||
|
||||
return toResponse(router);
|
||||
}
|
||||
|
||||
private String findNextAvailableIp(Set<String> usedIps) {
|
||||
|
||||
for (int thirdOctet = 1; thirdOctet <= 255; thirdOctet++) {
|
||||
|
||||
for (int fourthOctet = 2; fourthOctet <= 254; fourthOctet++) {
|
||||
|
||||
String ip = "198.19." + thirdOctet + "." + fourthOctet;
|
||||
|
||||
if (!usedIps.contains(ip)) {
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalStateException("No VPN IPs available");
|
||||
}
|
||||
|
||||
private RouterResponse toResponse(Router router) {
|
||||
|
||||
return new RouterResponse(
|
||||
router.getId(),
|
||||
router.getName(),
|
||||
router.getHardwareModel(),
|
||||
router.getFirmwareVersion(),
|
||||
router.getStatus(),
|
||||
router.getVpnIp(),
|
||||
router.getWireguardPublicKey(),
|
||||
router.getEndpointMode(),
|
||||
router.getVpnStatus(),
|
||||
router.getVpnProvisionedAt(),
|
||||
router.getCreatedAt(),
|
||||
router.getUpdatedAt()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,4 +17,12 @@ public class VpsController {
|
||||
public String health() {
|
||||
return wireGuardVpsService.getVpsHealthJson();
|
||||
}
|
||||
|
||||
@PostMapping(
|
||||
value = "/wireguard/rollback-last-backup",
|
||||
produces = MediaType.APPLICATION_JSON_VALUE
|
||||
)
|
||||
public String rollbackLastBackup() {
|
||||
return wireGuardVpsService.restoreLastWireGuardBackup();
|
||||
}
|
||||
}
|
||||
@@ -79,4 +79,18 @@ public class WireGuardVpsService {
|
||||
|
||||
return result.stdout();
|
||||
}
|
||||
|
||||
public String restoreLastWireGuardBackup() {
|
||||
SshCommandResult result = sshService.executeOnConfiguredVps(
|
||||
"sudo /usr/local/sbin/lr-wg-restore-last-backup"
|
||||
);
|
||||
|
||||
if (result.exitCode() != 0) {
|
||||
throw new SshCommandException(
|
||||
"Failed to restore WireGuard backup: " + result.stderr()
|
||||
);
|
||||
}
|
||||
|
||||
return result.stdout();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
ALTER TABLE routers
|
||||
ADD COLUMN wireguard_public_key VARCHAR(255),
|
||||
ADD COLUMN endpoint_mode VARCHAR(50),
|
||||
ADD COLUMN vpn_status VARCHAR(50),
|
||||
ADD COLUMN vpn_provisioned_at TIMESTAMP;
|
||||
|
||||
CREATE UNIQUE INDEX idx_routers_wireguard_public_key
|
||||
ON routers(wireguard_public_key)
|
||||
WHERE wireguard_public_key IS NOT NULL;
|
||||
Reference in New Issue
Block a user