Apply WireGuard peers to VPS safely

This commit is contained in:
litoral05
2026-05-07 15:10:09 +01:00
parent f93a4c8402
commit 8b849e6560
11 changed files with 309 additions and 0 deletions
@@ -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
);
}
} }
@@ -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);
}
}
@@ -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
) {
}
@@ -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();
}
}
@@ -0,0 +1,8 @@
package com.litoralregas.vpnprovisioner.wireguard.entity;
public enum WireGuardPeerStatus {
PENDING_APPLY,
APPLIED,
FAILED,
REVOKED
}
@@ -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);
}
@@ -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);