Move VPN provisioning onto router model
This commit is contained in:
+13
-1
@@ -1,9 +1,11 @@
|
|||||||
package com.litoralregas.vpnprovisioner.router.controller;
|
package com.litoralregas.vpnprovisioner.router.controller;
|
||||||
|
|
||||||
import com.litoralregas.vpnprovisioner.router.dto.CreateRouterRequest;
|
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.RouterResponse;
|
||||||
import com.litoralregas.vpnprovisioner.router.dto.UpdateRouterRequest;
|
import com.litoralregas.vpnprovisioner.router.dto.UpdateRouterRequest;
|
||||||
import com.litoralregas.vpnprovisioner.router.service.RouterService;
|
import com.litoralregas.vpnprovisioner.router.service.RouterService;
|
||||||
|
import com.litoralregas.vpnprovisioner.router.service.RouterVpnProvisioningService;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@@ -16,9 +18,11 @@ import java.util.UUID;
|
|||||||
public class RouterController {
|
public class RouterController {
|
||||||
|
|
||||||
private final RouterService routerService;
|
private final RouterService routerService;
|
||||||
|
private final RouterVpnProvisioningService routerVpnProvisioningService;
|
||||||
|
|
||||||
public RouterController(RouterService routerService) {
|
public RouterController(RouterService routerService, RouterVpnProvisioningService routerVpnProvisioningService) {
|
||||||
this.routerService = routerService;
|
this.routerService = routerService;
|
||||||
|
this.routerVpnProvisioningService = routerVpnProvisioningService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@@ -50,4 +54,12 @@ public class RouterController {
|
|||||||
public void delete(@PathVariable UUID id) {
|
public void delete(@PathVariable UUID id) {
|
||||||
routerService.delete(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;
|
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.RouterStatus;
|
||||||
|
import com.litoralregas.vpnprovisioner.router.entity.RouterVpnStatus;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -12,6 +14,10 @@ public record RouterResponse(
|
|||||||
String firmwareVersion,
|
String firmwareVersion,
|
||||||
RouterStatus status,
|
RouterStatus status,
|
||||||
String vpnIp,
|
String vpnIp,
|
||||||
|
String wireguardPublicKey,
|
||||||
|
EndpointMode endpointMode,
|
||||||
|
RouterVpnStatus vpnStatus,
|
||||||
|
Instant vpnProvisionedAt,
|
||||||
Instant createdAt,
|
Instant createdAt,
|
||||||
Instant updatedAt
|
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;
|
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)
|
@Column(nullable = false)
|
||||||
private Instant createdAt;
|
private Instant createdAt;
|
||||||
|
|
||||||
@@ -40,6 +51,7 @@ public class Router {
|
|||||||
this.hardwareModel = hardwareModel;
|
this.hardwareModel = hardwareModel;
|
||||||
this.firmwareVersion = firmwareVersion;
|
this.firmwareVersion = firmwareVersion;
|
||||||
this.status = RouterStatus.PENDING;
|
this.status = RouterStatus.PENDING;
|
||||||
|
this.vpnStatus = RouterVpnStatus.NOT_PROVISIONED;
|
||||||
this.createdAt = Instant.now();
|
this.createdAt = Instant.now();
|
||||||
this.updatedAt = Instant.now();
|
this.updatedAt = Instant.now();
|
||||||
}
|
}
|
||||||
@@ -49,15 +61,6 @@ public class Router {
|
|||||||
this.updatedAt = Instant.now();
|
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) {
|
public void updateDetails(String name, String hardwareModel, String firmwareVersion) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.hardwareModel = hardwareModel;
|
this.hardwareModel = hardwareModel;
|
||||||
@@ -67,4 +70,72 @@ public class Router {
|
|||||||
public void assignVpnIp(String vpnIp) {
|
public void assignVpnIp(String vpnIp) {
|
||||||
this.vpnIp = 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.getFirmwareVersion(),
|
||||||
router.getStatus(),
|
router.getStatus(),
|
||||||
router.getVpnIp(),
|
router.getVpnIp(),
|
||||||
|
router.getWireguardPublicKey(),
|
||||||
|
router.getEndpointMode(),
|
||||||
|
router.getVpnStatus(),
|
||||||
|
router.getVpnProvisionedAt(),
|
||||||
router.getCreatedAt(),
|
router.getCreatedAt(),
|
||||||
router.getUpdatedAt()
|
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() {
|
public String health() {
|
||||||
return wireGuardVpsService.getVpsHealthJson();
|
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();
|
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