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; 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);
}
} }
@@ -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()
); );
@@ -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;