Stable core service
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
package com.litoralregas.vpnorchestrator.auth;
|
||||
|
||||
import com.litoralregas.vpnorchestrator.config.AppSecurityProperties;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class ApiKeyAuthFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final String API_KEY_HEADER = "X-API-Key";
|
||||
|
||||
private final AppSecurityProperties securityProperties;
|
||||
|
||||
public ApiKeyAuthFilter(AppSecurityProperties securityProperties) {
|
||||
this.securityProperties = securityProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
return "/actuator/health".equals(request.getRequestURI());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain
|
||||
) throws ServletException, IOException {
|
||||
|
||||
String providedApiKey = request.getHeader(API_KEY_HEADER);
|
||||
String expectedApiKey = securityProperties.getApiKey();
|
||||
|
||||
if (!StringUtils.hasText(expectedApiKey)) {
|
||||
response.sendError(
|
||||
HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
|
||||
"API key is not configured"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!expectedApiKey.equals(providedApiKey)) {
|
||||
response.sendError(
|
||||
HttpServletResponse.SC_UNAUTHORIZED,
|
||||
"Invalid API key"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(
|
||||
"api-key-client",
|
||||
null,
|
||||
List.of(new SimpleGrantedAuthority("ROLE_API"))
|
||||
);
|
||||
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.litoralregas.vpnorchestrator.common.exception;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record ApiErrorResponse(
|
||||
Instant timestamp,
|
||||
int status,
|
||||
String error,
|
||||
String message,
|
||||
String path
|
||||
) {
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package com.litoralregas.vpnorchestrator.common.exception;
|
||||
|
||||
import com.litoralregas.vpnorchestrator.vps.SshCommandException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public ApiErrorResponse handleValidation(
|
||||
MethodArgumentNotValidException exception,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
String message = exception.getBindingResult()
|
||||
.getFieldErrors()
|
||||
.stream()
|
||||
.map(error -> error.getField() + ": " + error.getDefaultMessage())
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
return new ApiErrorResponse(
|
||||
Instant.now(),
|
||||
HttpStatus.BAD_REQUEST.value(),
|
||||
"Bad Request",
|
||||
message,
|
||||
request.getRequestURI()
|
||||
);
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public ApiErrorResponse handleIllegalArgument(
|
||||
IllegalArgumentException exception,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
return new ApiErrorResponse(
|
||||
Instant.now(),
|
||||
HttpStatus.BAD_REQUEST.value(),
|
||||
"Bad Request",
|
||||
exception.getMessage(),
|
||||
request.getRequestURI()
|
||||
);
|
||||
}
|
||||
|
||||
@ExceptionHandler(SshCommandException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_GATEWAY)
|
||||
public ApiErrorResponse handleSshCommand(
|
||||
SshCommandException exception,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
return new ApiErrorResponse(
|
||||
Instant.now(),
|
||||
HttpStatus.BAD_GATEWAY.value(),
|
||||
"Bad Gateway",
|
||||
exception.getMessage(),
|
||||
request.getRequestURI()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.litoralregas.vpnorchestrator.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(prefix = "app.security")
|
||||
public class AppSecurityProperties {
|
||||
|
||||
private String apiKey;
|
||||
|
||||
public String getApiKey() {
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
public void setApiKey(String apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.litoralregas.vpnorchestrator.config;
|
||||
|
||||
import com.litoralregas.vpnorchestrator.auth.ApiKeyAuthFilter;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties({
|
||||
AppSecurityProperties.class,
|
||||
VpsSshProperties.class
|
||||
})
|
||||
public class SecurityConfig {
|
||||
|
||||
private final AppSecurityProperties securityProperties;
|
||||
|
||||
public SecurityConfig(AppSecurityProperties securityProperties) {
|
||||
this.securityProperties = securityProperties;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
ApiKeyAuthFilter apiKeyAuthFilter = new ApiKeyAuthFilter(securityProperties);
|
||||
|
||||
return http
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.cors(cors -> cors.disable())
|
||||
.formLogin(form -> form.disable())
|
||||
.httpBasic(basic -> basic.disable())
|
||||
.logout(logout -> logout.disable())
|
||||
.sessionManagement(session -> session
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
)
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/actuator/health").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.addFilterBefore(apiKeyAuthFilter, UsernamePasswordAuthenticationFilter.class)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.litoralregas.vpnorchestrator.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(prefix = "app.vps.ssh")
|
||||
public class VpsSshProperties {
|
||||
|
||||
private String host;
|
||||
private int port = 22;
|
||||
private String username = "root";
|
||||
private String password;
|
||||
private String privateKeyPath;
|
||||
private int connectTimeoutMs = 10_000;
|
||||
private int commandTimeoutMs = 15_000;
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public void setHost(String host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public void setPort(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getPrivateKeyPath() {
|
||||
return privateKeyPath;
|
||||
}
|
||||
|
||||
public void setPrivateKeyPath(String privateKeyPath) {
|
||||
this.privateKeyPath = privateKeyPath;
|
||||
}
|
||||
|
||||
public int getConnectTimeoutMs() {
|
||||
return connectTimeoutMs;
|
||||
}
|
||||
|
||||
public void setConnectTimeoutMs(int connectTimeoutMs) {
|
||||
this.connectTimeoutMs = connectTimeoutMs;
|
||||
}
|
||||
|
||||
public int getCommandTimeoutMs() {
|
||||
return commandTimeoutMs;
|
||||
}
|
||||
|
||||
public void setCommandTimeoutMs(int commandTimeoutMs) {
|
||||
this.commandTimeoutMs = commandTimeoutMs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.litoralregas.vpnorchestrator.vpn;
|
||||
|
||||
import com.litoralregas.vpnorchestrator.vpn.dto.AvailableVpnIpResponse;
|
||||
import com.litoralregas.vpnorchestrator.vpn.dto.PeerApplyResponse;
|
||||
import com.litoralregas.vpnorchestrator.vpn.dto.UpsertPeerRequest;
|
||||
import com.litoralregas.vpnorchestrator.vpn.dto.UsedVpnIpsResponse;
|
||||
import com.litoralregas.vpnorchestrator.vps.WireGuardService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/vpn")
|
||||
public class VpnController {
|
||||
|
||||
private final WireGuardService wireGuardService;
|
||||
private final VpnIpAllocatorService vpnIpAllocatorService;
|
||||
|
||||
public VpnController(
|
||||
WireGuardService wireGuardService,
|
||||
VpnIpAllocatorService vpnIpAllocatorService
|
||||
) {
|
||||
this.wireGuardService = wireGuardService;
|
||||
this.vpnIpAllocatorService = vpnIpAllocatorService;
|
||||
}
|
||||
|
||||
@GetMapping("/available-ip")
|
||||
public AvailableVpnIpResponse availableIp() {
|
||||
Set<String> usedIps = wireGuardService.findUsedVpnIps();
|
||||
String vpnIp = vpnIpAllocatorService.nextAvailableIp(usedIps);
|
||||
|
||||
return new AvailableVpnIpResponse(vpnIp);
|
||||
}
|
||||
|
||||
@PostMapping("/peers")
|
||||
public PeerApplyResponse upsertPeer(@Valid @RequestBody UpsertPeerRequest request) {
|
||||
wireGuardService.applyPeer(
|
||||
request.publicKey(),
|
||||
request.vpnIp() + "/32"
|
||||
);
|
||||
|
||||
return new PeerApplyResponse(
|
||||
request.vpnIp(),
|
||||
request.publicKey(),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/used-ips")
|
||||
public UsedVpnIpsResponse usedIps() {
|
||||
Set<String> usedIps = wireGuardService.findUsedVpnIps();
|
||||
|
||||
return new UsedVpnIpsResponse(
|
||||
usedIps,
|
||||
usedIps.size()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.litoralregas.vpnorchestrator.vpn;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class VpnIpAllocatorService {
|
||||
|
||||
public String nextAvailableIp(Set<String> usedIps) {
|
||||
for (int third = 1; third <= 254; third++) {
|
||||
for (int fourth = 1; fourth <= 254; fourth++) {
|
||||
String candidate = "198.19." + third + "." + fourth;
|
||||
|
||||
if (!usedIps.contains(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalStateException("No available VPN IPs in 198.19.0.0/16 pool");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.litoralregas.vpnorchestrator.vpn.dto;
|
||||
|
||||
public record AvailableVpnIpResponse(
|
||||
String vpnIp
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.litoralregas.vpnorchestrator.vpn.dto;
|
||||
|
||||
public record PeerApplyResponse(
|
||||
String vpnIp,
|
||||
String publicKey,
|
||||
boolean applied
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.litoralregas.vpnorchestrator.vpn.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
|
||||
public record UpsertPeerRequest(
|
||||
|
||||
@NotBlank
|
||||
@Pattern(
|
||||
regexp = "^198\\.19\\.(?:[1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-4])\\.(?:[1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-4])$",
|
||||
message = "VPN IP must be inside allocatable range 198.19.1.1 - 198.19.254.254"
|
||||
)
|
||||
String vpnIp,
|
||||
|
||||
@NotBlank
|
||||
@Pattern(
|
||||
regexp = "^[A-Za-z0-9+/]{43}=$",
|
||||
message = "Invalid WireGuard public key format"
|
||||
)
|
||||
String publicKey
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.litoralregas.vpnorchestrator.vpn.dto;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public record UsedVpnIpsResponse(
|
||||
Set<String> usedIps,
|
||||
int count
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.litoralregas.vpnorchestrator.vps;
|
||||
|
||||
public class SshCommandException extends RuntimeException {
|
||||
|
||||
public SshCommandException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public SshCommandException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.litoralregas.vpnorchestrator.vps;
|
||||
|
||||
public record SshCommandResult(
|
||||
int exitCode,
|
||||
String stdout,
|
||||
String stderr
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.litoralregas.vpnorchestrator.vps;
|
||||
|
||||
import com.jcraft.jsch.ChannelExec;
|
||||
import com.jcraft.jsch.JSch;
|
||||
import com.jcraft.jsch.Session;
|
||||
import com.litoralregas.vpnorchestrator.config.VpsSshProperties;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Properties;
|
||||
|
||||
@Service
|
||||
public class SshService {
|
||||
|
||||
private final VpsSshProperties properties;
|
||||
|
||||
public SshService(VpsSshProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public SshCommandResult executeOnConfiguredVps(String command) {
|
||||
validateConfiguredVps();
|
||||
return execute(
|
||||
properties.getHost(),
|
||||
properties.getPort(),
|
||||
properties.getUsername(),
|
||||
properties.getPassword(),
|
||||
properties.getPrivateKeyPath(),
|
||||
command,
|
||||
properties.getConnectTimeoutMs(),
|
||||
properties.getCommandTimeoutMs()
|
||||
);
|
||||
}
|
||||
|
||||
public SshCommandResult execute(
|
||||
String host,
|
||||
int port,
|
||||
String username,
|
||||
String password,
|
||||
String privateKeyPath,
|
||||
String command,
|
||||
int connectTimeoutMs,
|
||||
int commandTimeoutMs
|
||||
) {
|
||||
Session session = null;
|
||||
ChannelExec channel = null;
|
||||
|
||||
try {
|
||||
JSch jsch = new JSch();
|
||||
|
||||
if (StringUtils.hasText(privateKeyPath)) {
|
||||
jsch.addIdentity(privateKeyPath);
|
||||
}
|
||||
|
||||
session = jsch.getSession(username, host, port);
|
||||
|
||||
if (StringUtils.hasText(password)) {
|
||||
session.setPassword(password);
|
||||
}
|
||||
|
||||
Properties config = new Properties();
|
||||
config.put("StrictHostKeyChecking", "no");
|
||||
session.setConfig(config);
|
||||
|
||||
session.connect(connectTimeoutMs);
|
||||
|
||||
channel = (ChannelExec) session.openChannel("exec");
|
||||
channel.setCommand(command);
|
||||
channel.setInputStream(null);
|
||||
|
||||
ByteArrayOutputStream stdout = new ByteArrayOutputStream();
|
||||
ByteArrayOutputStream stderr = new ByteArrayOutputStream();
|
||||
|
||||
channel.setOutputStream(stdout);
|
||||
channel.setErrStream(stderr);
|
||||
|
||||
channel.connect(connectTimeoutMs);
|
||||
|
||||
long deadline = System.currentTimeMillis() + commandTimeoutMs;
|
||||
|
||||
while (!channel.isClosed()) {
|
||||
if (System.currentTimeMillis() > deadline) {
|
||||
throw new SshCommandException("SSH command timed out: " + command);
|
||||
}
|
||||
|
||||
Thread.sleep(100);
|
||||
}
|
||||
|
||||
return new SshCommandResult(
|
||||
channel.getExitStatus(),
|
||||
stdout.toString(StandardCharsets.UTF_8),
|
||||
stderr.toString(StandardCharsets.UTF_8)
|
||||
);
|
||||
|
||||
} catch (SshCommandException exception) {
|
||||
throw exception;
|
||||
} catch (Exception exception) {
|
||||
throw new SshCommandException("SSH command failed: " + command, exception);
|
||||
} finally {
|
||||
if (channel != null) {
|
||||
channel.disconnect();
|
||||
}
|
||||
|
||||
if (session != null) {
|
||||
session.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateConfiguredVps() {
|
||||
if (!StringUtils.hasText(properties.getHost())) {
|
||||
throw new SshCommandException("VPS SSH host is not configured");
|
||||
}
|
||||
|
||||
if (!StringUtils.hasText(properties.getUsername())) {
|
||||
throw new SshCommandException("VPS SSH username is not configured");
|
||||
}
|
||||
|
||||
if (!StringUtils.hasText(properties.getPassword())
|
||||
&& !StringUtils.hasText(properties.getPrivateKeyPath())) {
|
||||
throw new SshCommandException("Either VPS SSH password or private key path must be configured");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.litoralregas.vpnorchestrator.vps;
|
||||
|
||||
import com.litoralregas.vpnorchestrator.vps.dto.VpsHealthResponse;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/vps")
|
||||
public class VpsController {
|
||||
|
||||
private final WireGuardService wireGuardService;
|
||||
|
||||
public VpsController(WireGuardService wireGuardService) {
|
||||
this.wireGuardService = wireGuardService;
|
||||
}
|
||||
|
||||
@GetMapping(value = "/health", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public VpsHealthResponse health() {
|
||||
return wireGuardService.getVpsHealth();
|
||||
}
|
||||
|
||||
@PostMapping(
|
||||
value = "/wireguard/rollback-last-backup",
|
||||
produces = MediaType.APPLICATION_JSON_VALUE
|
||||
)
|
||||
public String rollbackLastBackup() {
|
||||
return wireGuardService.restoreLastWireGuardBackup();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.litoralregas.vpnorchestrator.vps;
|
||||
|
||||
public record WireGuardPeerApplyResult(
|
||||
String publicKey,
|
||||
String allowedIps,
|
||||
boolean applied
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.litoralregas.vpnorchestrator.vps;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.litoralregas.vpnorchestrator.vps.dto.VpsHealthResponse;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Service
|
||||
public class WireGuardService {
|
||||
|
||||
private static final Pattern VPN_IP_PATTERN =
|
||||
Pattern.compile("\\b198\\.19\\.\\d{1,3}\\.\\d{1,3}\\b");
|
||||
|
||||
private final SshService sshService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public WireGuardService(SshService sshService, ObjectMapper objectMapper) {
|
||||
this.sshService = sshService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public Set<String> findUsedVpnIps() {
|
||||
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 parseVpnIps(result.stdout());
|
||||
}
|
||||
|
||||
Set<String> parseVpnIps(String output) {
|
||||
Set<String> ips = new HashSet<>();
|
||||
|
||||
Matcher matcher = VPN_IP_PATTERN.matcher(output);
|
||||
|
||||
while (matcher.find()) {
|
||||
ips.add(matcher.group());
|
||||
}
|
||||
|
||||
return ips;
|
||||
}
|
||||
|
||||
public WireGuardPeerApplyResult applyPeer(String publicKey, String allowedIps) {
|
||||
|
||||
String command = "sudo /usr/local/sbin/lr-wg-add-peer "
|
||||
+ shellQuote(publicKey) + " "
|
||||
+ shellQuote(allowedIps);
|
||||
|
||||
SshCommandResult result = sshService.executeOnConfiguredVps(command);
|
||||
|
||||
if (result.exitCode() != 0) {
|
||||
throw new SshCommandException(
|
||||
"Failed to apply WireGuard peer: " + result.stderr() + result.stdout()
|
||||
);
|
||||
}
|
||||
|
||||
return new WireGuardPeerApplyResult(
|
||||
publicKey,
|
||||
allowedIps,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
private String shellQuote(String value) {
|
||||
return "'" + value.replace("'", "'\"'\"'") + "'";
|
||||
}
|
||||
|
||||
public VpsHealthResponse getVpsHealth() {
|
||||
SshCommandResult result = sshService.executeOnConfiguredVps(
|
||||
"sudo /usr/local/sbin/lr-vps-health"
|
||||
);
|
||||
|
||||
if (result.exitCode() != 0) {
|
||||
throw new SshCommandException(
|
||||
"Failed to query VPS health: " + result.stderr()
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
return objectMapper.readValue(result.stdout(), VpsHealthResponse.class);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalStateException(
|
||||
"Invalid VPS health JSON returned by script",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.litoralregas.vpnorchestrator.vps.dto;
|
||||
|
||||
public record VpsHealthResponse(
|
||||
String wireGuardInterface,
|
||||
boolean wireGuardRunning,
|
||||
int wireGuardPeerCount,
|
||||
boolean wireGuardConfigExists,
|
||||
String udp2rawService,
|
||||
boolean udp2rawActive,
|
||||
String latestWireGuardBackup,
|
||||
String systemUptime,
|
||||
int diskUsagePercent,
|
||||
int memoryUsagePercent,
|
||||
String loadAverage,
|
||||
String publicIp
|
||||
) {
|
||||
}
|
||||
@@ -1,3 +1,25 @@
|
||||
spring:
|
||||
application:
|
||||
name: vpn-orchestrator
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health
|
||||
|
||||
app:
|
||||
security:
|
||||
api-key: dev-api-key
|
||||
|
||||
vps:
|
||||
ssh:
|
||||
host: 146.59.230.190
|
||||
port: 22
|
||||
username: lr-vpn
|
||||
password: hidrotek2026
|
||||
connect-timeout-ms: 10000
|
||||
command-timeout-ms: 15000
|
||||
Reference in New Issue
Block a user