Apply WireGuard peers to VPS safely
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
package com.litoralregas.vpnprovisioner.vps;
|
||||||
|
|
||||||
|
public record WireGuardPeerApplyResult(
|
||||||
|
String publicKey,
|
||||||
|
String allowedIps,
|
||||||
|
boolean applied
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -44,4 +44,25 @@ public class WireGuardVpsService {
|
|||||||
|
|
||||||
return ips;
|
return ips;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public WireGuardPeerApplyResult applyPeer(String publicKey, String allowedIps) {
|
||||||
|
|
||||||
|
String command = """
|
||||||
|
sudo /usr/local/sbin/lr-wg-add-peer '%s' '%s'
|
||||||
|
""".formatted(publicKey, allowedIps);
|
||||||
|
|
||||||
|
SshCommandResult result = sshService.executeOnConfiguredVps(command);
|
||||||
|
|
||||||
|
if (result.exitCode() != 0) {
|
||||||
|
throw new SshCommandException(
|
||||||
|
"Failed to apply WireGuard peer: " + result.stderr()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WireGuardPeerApplyResult(
|
||||||
|
publicKey,
|
||||||
|
allowedIps,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
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
@@ -0,0 +1,11 @@
|
|||||||
|
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
@@ -0,0 +1,20 @@
|
|||||||
|
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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.litoralregas.vpnprovisioner.wireguard.entity;
|
||||||
|
|
||||||
|
public enum EndpointMode {
|
||||||
|
NORMAL_WIREGUARD,
|
||||||
|
UDP2RAW
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
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
@@ -0,0 +1,8 @@
|
|||||||
|
package com.litoralregas.vpnprovisioner.wireguard.entity;
|
||||||
|
|
||||||
|
public enum WireGuardPeerStatus {
|
||||||
|
PENDING_APPLY,
|
||||||
|
APPLIED,
|
||||||
|
FAILED,
|
||||||
|
REVOKED
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
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
@@ -0,0 +1,93 @@
|
|||||||
|
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,18 @@
|
|||||||
|
CREATE TABLE wireguard_peers (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
router_id UUID NOT NULL UNIQUE,
|
||||||
|
public_key VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
vpn_ip VARCHAR(45) NOT NULL,
|
||||||
|
allowed_ips VARCHAR(100) NOT NULL,
|
||||||
|
endpoint_mode VARCHAR(50) NOT NULL,
|
||||||
|
status VARCHAR(50) NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT fk_wireguard_peers_router
|
||||||
|
FOREIGN KEY (router_id)
|
||||||
|
REFERENCES routers(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_wireguard_peers_status
|
||||||
|
ON wireguard_peers(status);
|
||||||
Reference in New Issue
Block a user