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;
|
||||
}
|
||||
|
||||
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