Add WireGuard VPS sync bootstrap flow
This commit is contained in:
-30
@@ -1,30 +0,0 @@
|
|||||||
package com.litoralregas.vpnprovisioner.ipam.controller;
|
|
||||||
|
|
||||||
import com.litoralregas.vpnprovisioner.ipam.dto.VpnIpAllocationResponse;
|
|
||||||
import com.litoralregas.vpnprovisioner.ipam.service.VpnIpAllocationService;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/ipam")
|
|
||||||
public class VpnIpAllocationController {
|
|
||||||
|
|
||||||
private final VpnIpAllocationService service;
|
|
||||||
|
|
||||||
public VpnIpAllocationController(VpnIpAllocationService service) {
|
|
||||||
this.service = service;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/routers/{routerId}/allocate")
|
|
||||||
public VpnIpAllocationResponse allocate(@PathVariable UUID routerId) {
|
|
||||||
return service.allocateForRouter(routerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/routers/{routerId}/release")
|
|
||||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
|
||||||
public void release(@PathVariable UUID routerId) {
|
|
||||||
service.releaseForRouter(routerId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.litoralregas.vpnprovisioner.ipam.dto;
|
|
||||||
|
|
||||||
import com.litoralregas.vpnprovisioner.ipam.entity.VpnIpAllocationStatus;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public record VpnIpAllocationResponse(
|
|
||||||
UUID id,
|
|
||||||
String ipAddress,
|
|
||||||
VpnIpAllocationStatus status,
|
|
||||||
UUID routerId,
|
|
||||||
Instant allocatedAt,
|
|
||||||
Instant releasedAt
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package com.litoralregas.vpnprovisioner.ipam.entity;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "vpn_ip_allocations")
|
|
||||||
public class VpnIpAllocation {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
private UUID id;
|
|
||||||
|
|
||||||
@Column(nullable = false, unique = true)
|
|
||||||
private String ipAddress;
|
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
|
||||||
@Column(nullable = false)
|
|
||||||
private VpnIpAllocationStatus status;
|
|
||||||
|
|
||||||
private UUID routerId;
|
|
||||||
|
|
||||||
private Instant allocatedAt;
|
|
||||||
|
|
||||||
private Instant releasedAt;
|
|
||||||
|
|
||||||
protected VpnIpAllocation() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public VpnIpAllocation(String ipAddress) {
|
|
||||||
this.id = UUID.randomUUID();
|
|
||||||
this.ipAddress = ipAddress;
|
|
||||||
this.status = VpnIpAllocationStatus.ALLOCATED_EXTERNAL;
|
|
||||||
this.allocatedAt = Instant.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static VpnIpAllocation backendAllocated(String ipAddress, UUID routerId) {
|
|
||||||
VpnIpAllocation allocation = new VpnIpAllocation(ipAddress);
|
|
||||||
allocation.allocateTo(routerId);
|
|
||||||
return allocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void allocateTo(UUID routerId) {
|
|
||||||
this.status = VpnIpAllocationStatus.ALLOCATED;
|
|
||||||
this.routerId = routerId;
|
|
||||||
this.allocatedAt = Instant.now();
|
|
||||||
this.releasedAt = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void markExternal() {
|
|
||||||
if (this.status != VpnIpAllocationStatus.ALLOCATED) {
|
|
||||||
this.status = VpnIpAllocationStatus.ALLOCATED_EXTERNAL;
|
|
||||||
this.allocatedAt = this.allocatedAt == null ? Instant.now() : this.allocatedAt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void release() {
|
|
||||||
this.status = VpnIpAllocationStatus.RELEASED;
|
|
||||||
this.routerId = null;
|
|
||||||
this.releasedAt = Instant.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
public UUID getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getIpAddress() {
|
|
||||||
return ipAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
public VpnIpAllocationStatus getStatus() {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public UUID getRouterId() {
|
|
||||||
return routerId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Instant getAllocatedAt() {
|
|
||||||
return allocatedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Instant getReleasedAt() {
|
|
||||||
return releasedAt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package com.litoralregas.vpnprovisioner.ipam.entity;
|
|
||||||
|
|
||||||
public enum VpnIpAllocationStatus {
|
|
||||||
ALLOCATED,
|
|
||||||
ALLOCATED_EXTERNAL,
|
|
||||||
RELEASED,
|
|
||||||
RETIRED
|
|
||||||
}
|
|
||||||
-25
@@ -1,25 +0,0 @@
|
|||||||
package com.litoralregas.vpnprovisioner.ipam.repository;
|
|
||||||
|
|
||||||
import com.litoralregas.vpnprovisioner.ipam.entity.VpnIpAllocation;
|
|
||||||
import org.springframework.data.jpa.repository.*;
|
|
||||||
import org.springframework.data.repository.query.Param;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public interface VpnIpAllocationRepository extends JpaRepository<VpnIpAllocation, UUID> {
|
|
||||||
|
|
||||||
Optional<VpnIpAllocation> findByRouterId(UUID routerId);
|
|
||||||
|
|
||||||
Optional<VpnIpAllocation> findByIpAddress(String ipAddress);
|
|
||||||
|
|
||||||
@Modifying
|
|
||||||
@Query("""
|
|
||||||
UPDATE VpnIpAllocation ip
|
|
||||||
SET ip.status = 'RELEASED',
|
|
||||||
ip.routerId = null,
|
|
||||||
ip.releasedAt = CURRENT_TIMESTAMP
|
|
||||||
WHERE ip.routerId = :routerId
|
|
||||||
""")
|
|
||||||
void releaseByRouterId(@Param("routerId") UUID routerId);
|
|
||||||
}
|
|
||||||
-111
@@ -1,111 +0,0 @@
|
|||||||
package com.litoralregas.vpnprovisioner.ipam.service;
|
|
||||||
|
|
||||||
import com.litoralregas.vpnprovisioner.common.exception.ResourceNotFoundException;
|
|
||||||
import com.litoralregas.vpnprovisioner.ipam.dto.VpnIpAllocationResponse;
|
|
||||||
import com.litoralregas.vpnprovisioner.ipam.entity.VpnIpAllocation;
|
|
||||||
import com.litoralregas.vpnprovisioner.ipam.repository.VpnIpAllocationRepository;
|
|
||||||
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.util.Set;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class VpnIpAllocationService {
|
|
||||||
|
|
||||||
private static final int START_THIRD_OCTET = 1;
|
|
||||||
private static final int END_THIRD_OCTET = 255;
|
|
||||||
private static final int START_FOURTH_OCTET = 1;
|
|
||||||
private static final int END_FOURTH_OCTET = 254;
|
|
||||||
|
|
||||||
private final VpnIpAllocationRepository vpnIpAllocationRepository;
|
|
||||||
private final RouterRepository routerRepository;
|
|
||||||
private final WireGuardVpsService wireGuardVpsService;
|
|
||||||
|
|
||||||
public VpnIpAllocationService(
|
|
||||||
VpnIpAllocationRepository vpnIpAllocationRepository,
|
|
||||||
RouterRepository routerRepository,
|
|
||||||
WireGuardVpsService wireGuardVpsService
|
|
||||||
) {
|
|
||||||
this.vpnIpAllocationRepository = vpnIpAllocationRepository;
|
|
||||||
this.routerRepository = routerRepository;
|
|
||||||
this.wireGuardVpsService = wireGuardVpsService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public VpnIpAllocationResponse allocateForRouter(UUID routerId) {
|
|
||||||
Router router = routerRepository.findById(routerId)
|
|
||||||
.orElseThrow(() -> new ResourceNotFoundException("Router not found: " + routerId));
|
|
||||||
|
|
||||||
return vpnIpAllocationRepository.findByRouterId(routerId)
|
|
||||||
.map(this::toResponse)
|
|
||||||
.orElseGet(() -> allocateNewIp(router));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public void releaseForRouter(UUID routerId) {
|
|
||||||
vpnIpAllocationRepository.releaseByRouterId(routerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private VpnIpAllocationResponse allocateNewIp(Router router) {
|
|
||||||
Set<String> usedOnVps = refreshUsedIpsFromVps();
|
|
||||||
|
|
||||||
Set<String> knownInDb = vpnIpAllocationRepository.findAll()
|
|
||||||
.stream()
|
|
||||||
.map(VpnIpAllocation::getIpAddress)
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
|
|
||||||
String nextIp = findNextAvailableIp(usedOnVps, knownInDb);
|
|
||||||
|
|
||||||
VpnIpAllocation allocation = VpnIpAllocation.backendAllocated(nextIp, router.getId());
|
|
||||||
|
|
||||||
VpnIpAllocation saved = vpnIpAllocationRepository.save(allocation);
|
|
||||||
|
|
||||||
router.assignVpnIp(saved.getIpAddress());
|
|
||||||
|
|
||||||
return toResponse(saved);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Set<String> refreshUsedIpsFromVps() {
|
|
||||||
Set<String> usedIps = wireGuardVpsService.findUsedVpnIps();
|
|
||||||
|
|
||||||
for (String ip : usedIps) {
|
|
||||||
vpnIpAllocationRepository.findByIpAddress(ip)
|
|
||||||
.ifPresentOrElse(
|
|
||||||
VpnIpAllocation::markExternal,
|
|
||||||
() -> vpnIpAllocationRepository.save(new VpnIpAllocation(ip))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return usedIps;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String findNextAvailableIp(Set<String> usedOnVps, Set<String> knownInDb) {
|
|
||||||
for (int thirdOctet = START_THIRD_OCTET; thirdOctet <= END_THIRD_OCTET; thirdOctet++) {
|
|
||||||
for (int fourthOctet = START_FOURTH_OCTET; fourthOctet <= END_FOURTH_OCTET; fourthOctet++) {
|
|
||||||
String ip = "198.19." + thirdOctet + "." + fourthOctet;
|
|
||||||
|
|
||||||
if (!usedOnVps.contains(ip) && !knownInDb.contains(ip)) {
|
|
||||||
return ip;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new IllegalStateException("No free VPN IP addresses available");
|
|
||||||
}
|
|
||||||
|
|
||||||
private VpnIpAllocationResponse toResponse(VpnIpAllocation allocation) {
|
|
||||||
return new VpnIpAllocationResponse(
|
|
||||||
allocation.getId(),
|
|
||||||
allocation.getIpAddress(),
|
|
||||||
allocation.getStatus(),
|
|
||||||
allocation.getRouterId(),
|
|
||||||
allocation.getAllocatedAt(),
|
|
||||||
allocation.getReleasedAt()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+10
-5
@@ -1,10 +1,8 @@
|
|||||||
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.*;
|
||||||
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.RouterService;
|
||||||
|
import com.litoralregas.vpnprovisioner.router.service.RouterSyncService;
|
||||||
import com.litoralregas.vpnprovisioner.router.service.RouterVpnProvisioningService;
|
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;
|
||||||
@@ -19,10 +17,12 @@ public class RouterController {
|
|||||||
|
|
||||||
private final RouterService routerService;
|
private final RouterService routerService;
|
||||||
private final RouterVpnProvisioningService routerVpnProvisioningService;
|
private final RouterVpnProvisioningService routerVpnProvisioningService;
|
||||||
|
private final RouterSyncService routerSyncService;
|
||||||
|
|
||||||
public RouterController(RouterService routerService, RouterVpnProvisioningService routerVpnProvisioningService) {
|
public RouterController(RouterService routerService, RouterVpnProvisioningService routerVpnProvisioningService, RouterSyncService routerSyncService) {
|
||||||
this.routerService = routerService;
|
this.routerService = routerService;
|
||||||
this.routerVpnProvisioningService = routerVpnProvisioningService;
|
this.routerVpnProvisioningService = routerVpnProvisioningService;
|
||||||
|
this.routerSyncService = routerSyncService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@@ -62,4 +62,9 @@ public class RouterController {
|
|||||||
) {
|
) {
|
||||||
return routerVpnProvisioningService.provision(id, request);
|
return routerVpnProvisioningService.provision(id, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/sync-from-vps")
|
||||||
|
public SyncRoutersFromVpsResponse syncFromVps() {
|
||||||
|
return routerSyncService.syncFromVps();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
package com.litoralregas.vpnprovisioner.router.dto;
|
||||||
|
|
||||||
|
public record SyncRoutersFromVpsResponse(
|
||||||
|
int seenOnVps,
|
||||||
|
int created,
|
||||||
|
int updated
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -138,4 +138,31 @@ public class Router {
|
|||||||
public Instant getUpdatedAt() {
|
public Instant getUpdatedAt() {
|
||||||
return updatedAt;
|
return updatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Router importedVpnRouter(String publicKey, String vpnIp) {
|
||||||
|
Router router = new Router(
|
||||||
|
"Imported Router " + vpnIp,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
router.wireguardPublicKey = publicKey;
|
||||||
|
router.vpnIp = vpnIp;
|
||||||
|
router.endpointMode = EndpointMode.UDP2RAW;
|
||||||
|
router.vpnStatus = RouterVpnStatus.APPLIED;
|
||||||
|
router.vpnProvisionedAt = Instant.now();
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void syncVpnPeer(String publicKey, String vpnIp) {
|
||||||
|
this.wireguardPublicKey = publicKey;
|
||||||
|
this.vpnIp = vpnIp;
|
||||||
|
this.endpointMode = EndpointMode.UDP2RAW;
|
||||||
|
this.vpnStatus = RouterVpnStatus.APPLIED;
|
||||||
|
|
||||||
|
if (this.vpnProvisionedAt == null) {
|
||||||
|
this.vpnProvisionedAt = Instant.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,9 @@ package com.litoralregas.vpnprovisioner.router.repository;
|
|||||||
import com.litoralregas.vpnprovisioner.router.entity.Router;
|
import com.litoralregas.vpnprovisioner.router.entity.Router;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public interface RouterRepository extends JpaRepository<Router, UUID> {
|
public interface RouterRepository extends JpaRepository<Router, UUID> {
|
||||||
|
Optional<Router> findByWireguardPublicKey(String wireguardPublicKey);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package com.litoralregas.vpnprovisioner.router.service;
|
||||||
|
|
||||||
|
import com.litoralregas.vpnprovisioner.router.dto.SyncRoutersFromVpsResponse;
|
||||||
|
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.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class RouterSyncService {
|
||||||
|
|
||||||
|
private static final Pattern LINE_PATTERN =
|
||||||
|
Pattern.compile("^(.+?)\\s+([0-9.]+/32)$");
|
||||||
|
|
||||||
|
private final WireGuardVpsService wireGuardVpsService;
|
||||||
|
private final RouterRepository routerRepository;
|
||||||
|
|
||||||
|
public RouterSyncService(
|
||||||
|
WireGuardVpsService wireGuardVpsService,
|
||||||
|
RouterRepository routerRepository
|
||||||
|
) {
|
||||||
|
this.wireGuardVpsService = wireGuardVpsService;
|
||||||
|
this.routerRepository = routerRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public SyncRoutersFromVpsResponse syncFromVps() {
|
||||||
|
|
||||||
|
String output = wireGuardVpsService.showAllowedIps();
|
||||||
|
|
||||||
|
List<String> lines = output.lines()
|
||||||
|
.filter(line -> !line.isBlank())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
int created = 0;
|
||||||
|
int updated = 0;
|
||||||
|
|
||||||
|
for (String line : lines) {
|
||||||
|
|
||||||
|
Matcher matcher = LINE_PATTERN.matcher(line.trim());
|
||||||
|
|
||||||
|
if (!matcher.matches()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String publicKey = matcher.group(1).trim();
|
||||||
|
String allowedIp = matcher.group(2).trim();
|
||||||
|
|
||||||
|
String vpnIp = allowedIp.replace("/32", "");
|
||||||
|
|
||||||
|
Router router = routerRepository
|
||||||
|
.findByWireguardPublicKey(publicKey)
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (router == null) {
|
||||||
|
|
||||||
|
Router imported = Router.importedVpnRouter(
|
||||||
|
publicKey,
|
||||||
|
vpnIp
|
||||||
|
);
|
||||||
|
|
||||||
|
routerRepository.save(imported);
|
||||||
|
|
||||||
|
created++;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
router.syncVpnPeer(publicKey, vpnIp);
|
||||||
|
|
||||||
|
routerRepository.save(router);
|
||||||
|
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SyncRoutersFromVpsResponse(
|
||||||
|
lines.size(),
|
||||||
|
created,
|
||||||
|
updated
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.litoralregas.vpnprovisioner.router.startup;
|
||||||
|
|
||||||
|
import com.litoralregas.vpnprovisioner.router.service.RouterSyncService;
|
||||||
|
import org.springframework.boot.ApplicationArguments;
|
||||||
|
import org.springframework.boot.ApplicationRunner;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class RouterStartupSync implements ApplicationRunner {
|
||||||
|
|
||||||
|
private final RouterSyncService routerSyncService;
|
||||||
|
|
||||||
|
public RouterStartupSync(RouterSyncService routerSyncService) {
|
||||||
|
this.routerSyncService = routerSyncService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(ApplicationArguments args) {
|
||||||
|
|
||||||
|
routerSyncService.syncFromVps();
|
||||||
|
|
||||||
|
System.out.println("Router VPS sync completed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,4 +93,18 @@ public class WireGuardVpsService {
|
|||||||
|
|
||||||
return result.stdout();
|
return result.stdout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String showAllowedIps() {
|
||||||
|
SshCommandResult result = sshService.executeOnConfiguredVps(
|
||||||
|
"sudo /usr/local/sbin/lr-wg-used-ips"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.exitCode() != 0) {
|
||||||
|
throw new SshCommandException(
|
||||||
|
"Failed to query WireGuard allowed IPs: " + result.stderr()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.stdout();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
-30
@@ -1,30 +0,0 @@
|
|||||||
package com.litoralregas.vpnprovisioner.wireguard.controller;
|
|
||||||
|
|
||||||
import com.litoralregas.vpnprovisioner.wireguard.dto.CreateWireGuardPeerRequest;
|
|
||||||
import com.litoralregas.vpnprovisioner.wireguard.dto.WireGuardPeerResponse;
|
|
||||||
import com.litoralregas.vpnprovisioner.wireguard.service.WireGuardPeerService;
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/routers/{routerId}/wireguard-peer")
|
|
||||||
public class WireGuardPeerController {
|
|
||||||
|
|
||||||
private final WireGuardPeerService wireGuardPeerService;
|
|
||||||
|
|
||||||
public WireGuardPeerController(WireGuardPeerService wireGuardPeerService) {
|
|
||||||
this.wireGuardPeerService = wireGuardPeerService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping
|
|
||||||
@ResponseStatus(HttpStatus.CREATED)
|
|
||||||
public WireGuardPeerResponse create(
|
|
||||||
@PathVariable UUID routerId,
|
|
||||||
@Valid @RequestBody CreateWireGuardPeerRequest request
|
|
||||||
) {
|
|
||||||
return wireGuardPeerService.createForRouter(routerId, request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
package com.litoralregas.vpnprovisioner.wireguard.dto;
|
|
||||||
|
|
||||||
import com.litoralregas.vpnprovisioner.wireguard.entity.EndpointMode;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
|
|
||||||
public record CreateWireGuardPeerRequest(
|
|
||||||
@NotBlank String publicKey,
|
|
||||||
@NotNull EndpointMode endpointMode
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
-20
@@ -1,20 +0,0 @@
|
|||||||
package com.litoralregas.vpnprovisioner.wireguard.dto;
|
|
||||||
|
|
||||||
import com.litoralregas.vpnprovisioner.wireguard.entity.EndpointMode;
|
|
||||||
import com.litoralregas.vpnprovisioner.wireguard.entity.WireGuardPeerStatus;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public record WireGuardPeerResponse(
|
|
||||||
UUID id,
|
|
||||||
UUID routerId,
|
|
||||||
String publicKey,
|
|
||||||
String vpnIp,
|
|
||||||
String allowedIps,
|
|
||||||
EndpointMode endpointMode,
|
|
||||||
WireGuardPeerStatus status,
|
|
||||||
Instant createdAt,
|
|
||||||
Instant updatedAt
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package com.litoralregas.vpnprovisioner.wireguard.entity;
|
|
||||||
|
|
||||||
public enum EndpointMode {
|
|
||||||
NORMAL_WIREGUARD,
|
|
||||||
UDP2RAW
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
package com.litoralregas.vpnprovisioner.wireguard.entity;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "wireguard_peers")
|
|
||||||
public class WireGuardPeer {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
private UUID id;
|
|
||||||
|
|
||||||
@Column(nullable = false, unique = true)
|
|
||||||
private UUID routerId;
|
|
||||||
|
|
||||||
@Column(nullable = false, unique = true)
|
|
||||||
private String publicKey;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private String vpnIp;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private String allowedIps;
|
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
|
||||||
@Column(nullable = false)
|
|
||||||
private EndpointMode endpointMode;
|
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
|
||||||
@Column(nullable = false)
|
|
||||||
private WireGuardPeerStatus status;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private Instant createdAt;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private Instant updatedAt;
|
|
||||||
|
|
||||||
protected WireGuardPeer() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public WireGuardPeer(UUID routerId, String publicKey, String vpnIp, EndpointMode endpointMode) {
|
|
||||||
this.id = UUID.randomUUID();
|
|
||||||
this.routerId = routerId;
|
|
||||||
this.publicKey = publicKey;
|
|
||||||
this.vpnIp = vpnIp;
|
|
||||||
this.allowedIps = vpnIp + "/32";
|
|
||||||
this.endpointMode = endpointMode;
|
|
||||||
this.status = WireGuardPeerStatus.PENDING_APPLY;
|
|
||||||
this.createdAt = Instant.now();
|
|
||||||
this.updatedAt = Instant.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreUpdate
|
|
||||||
public void preUpdate() {
|
|
||||||
this.updatedAt = Instant.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
public UUID getId() { return id; }
|
|
||||||
public UUID getRouterId() { return routerId; }
|
|
||||||
public String getPublicKey() { return publicKey; }
|
|
||||||
public String getVpnIp() { return vpnIp; }
|
|
||||||
public String getAllowedIps() { return allowedIps; }
|
|
||||||
public EndpointMode getEndpointMode() { return endpointMode; }
|
|
||||||
public WireGuardPeerStatus getStatus() { return status; }
|
|
||||||
public Instant getCreatedAt() { return createdAt; }
|
|
||||||
public Instant getUpdatedAt() { return updatedAt; }
|
|
||||||
|
|
||||||
public void markApplied() {
|
|
||||||
this.status = WireGuardPeerStatus.APPLIED;
|
|
||||||
this.updatedAt = Instant.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void markFailed() {
|
|
||||||
this.status = WireGuardPeerStatus.FAILED;
|
|
||||||
this.updatedAt = Instant.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-8
@@ -1,8 +0,0 @@
|
|||||||
package com.litoralregas.vpnprovisioner.wireguard.entity;
|
|
||||||
|
|
||||||
public enum WireGuardPeerStatus {
|
|
||||||
PENDING_APPLY,
|
|
||||||
APPLIED,
|
|
||||||
FAILED,
|
|
||||||
REVOKED
|
|
||||||
}
|
|
||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
package com.litoralregas.vpnprovisioner.wireguard.repository;
|
|
||||||
|
|
||||||
import com.litoralregas.vpnprovisioner.wireguard.entity.WireGuardPeer;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public interface WireGuardPeerRepository extends JpaRepository<WireGuardPeer, UUID> {
|
|
||||||
|
|
||||||
Optional<WireGuardPeer> findByRouterId(UUID routerId);
|
|
||||||
|
|
||||||
boolean existsByPublicKey(String publicKey);
|
|
||||||
}
|
|
||||||
-93
@@ -1,93 +0,0 @@
|
|||||||
package com.litoralregas.vpnprovisioner.wireguard.service;
|
|
||||||
|
|
||||||
import com.litoralregas.vpnprovisioner.common.exception.ResourceNotFoundException;
|
|
||||||
import com.litoralregas.vpnprovisioner.ipam.dto.VpnIpAllocationResponse;
|
|
||||||
import com.litoralregas.vpnprovisioner.ipam.service.VpnIpAllocationService;
|
|
||||||
import com.litoralregas.vpnprovisioner.router.repository.RouterRepository;
|
|
||||||
import com.litoralregas.vpnprovisioner.vps.WireGuardVpsService;
|
|
||||||
import com.litoralregas.vpnprovisioner.wireguard.dto.CreateWireGuardPeerRequest;
|
|
||||||
import com.litoralregas.vpnprovisioner.wireguard.dto.WireGuardPeerResponse;
|
|
||||||
import com.litoralregas.vpnprovisioner.wireguard.entity.WireGuardPeer;
|
|
||||||
import com.litoralregas.vpnprovisioner.wireguard.repository.WireGuardPeerRepository;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class WireGuardPeerService {
|
|
||||||
|
|
||||||
private final WireGuardPeerRepository wireGuardPeerRepository;
|
|
||||||
private final RouterRepository routerRepository;
|
|
||||||
private final VpnIpAllocationService vpnIpAllocationService;
|
|
||||||
private final WireGuardVpsService wireGuardVpsService;
|
|
||||||
|
|
||||||
public WireGuardPeerService(
|
|
||||||
WireGuardPeerRepository wireGuardPeerRepository,
|
|
||||||
RouterRepository routerRepository,
|
|
||||||
VpnIpAllocationService vpnIpAllocationService,
|
|
||||||
WireGuardVpsService wireGuardVpsService) {
|
|
||||||
this.wireGuardPeerRepository = wireGuardPeerRepository;
|
|
||||||
this.routerRepository = routerRepository;
|
|
||||||
this.vpnIpAllocationService = vpnIpAllocationService;
|
|
||||||
this.wireGuardVpsService = wireGuardVpsService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public WireGuardPeerResponse createForRouter(UUID routerId, CreateWireGuardPeerRequest request) {
|
|
||||||
routerRepository.findById(routerId)
|
|
||||||
.orElseThrow(() -> new ResourceNotFoundException("Router not found: " + routerId));
|
|
||||||
|
|
||||||
return wireGuardPeerRepository.findByRouterId(routerId)
|
|
||||||
.map(this::toResponse)
|
|
||||||
.orElseGet(() -> createNewPeer(routerId, request));
|
|
||||||
}
|
|
||||||
|
|
||||||
private WireGuardPeerResponse createNewPeer(UUID routerId, CreateWireGuardPeerRequest request) {
|
|
||||||
if (wireGuardPeerRepository.existsByPublicKey(request.publicKey())) {
|
|
||||||
throw new IllegalArgumentException("WireGuard public key already exists");
|
|
||||||
}
|
|
||||||
|
|
||||||
VpnIpAllocationResponse allocation =
|
|
||||||
vpnIpAllocationService.allocateForRouter(routerId);
|
|
||||||
|
|
||||||
WireGuardPeer peer = new WireGuardPeer(
|
|
||||||
routerId,
|
|
||||||
request.publicKey(),
|
|
||||||
allocation.ipAddress(),
|
|
||||||
request.endpointMode()
|
|
||||||
);
|
|
||||||
|
|
||||||
WireGuardPeer saved = wireGuardPeerRepository.save(peer);
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
|
||||||
wireGuardVpsService.applyPeer(
|
|
||||||
saved.getPublicKey(),
|
|
||||||
saved.getAllowedIps()
|
|
||||||
);
|
|
||||||
|
|
||||||
saved.markApplied();
|
|
||||||
|
|
||||||
} catch (RuntimeException exception) {
|
|
||||||
saved.markFailed();
|
|
||||||
throw exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
return toResponse(saved);
|
|
||||||
}
|
|
||||||
|
|
||||||
private WireGuardPeerResponse toResponse(WireGuardPeer peer) {
|
|
||||||
return new WireGuardPeerResponse(
|
|
||||||
peer.getId(),
|
|
||||||
peer.getRouterId(),
|
|
||||||
peer.getPublicKey(),
|
|
||||||
peer.getVpnIp(),
|
|
||||||
peer.getAllowedIps(),
|
|
||||||
peer.getEndpointMode(),
|
|
||||||
peer.getStatus(),
|
|
||||||
peer.getCreatedAt(),
|
|
||||||
peer.getUpdatedAt()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
DROP TABLE IF EXISTS wireguard_peers;
|
||||||
|
DROP TABLE IF EXISTS vpn_ip_allocations;
|
||||||
Reference in New Issue
Block a user