Move VPN provisioning onto router model

This commit is contained in:
litoral05
2026-05-07 16:05:32 +01:00
parent 66adf1b42b
commit fe2faa4784
11 changed files with 268 additions and 10 deletions
@@ -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);
}
}
@@ -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()
);
@@ -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;