Skip to content

Commit

Permalink
Merge pull request #38 from FokiDoki/CustomPeerCreateParameters
Browse files Browse the repository at this point in the history
Create peer with custom parameters
  • Loading branch information
FokiDoki authored Jul 8, 2023
2 parents e8804b3 + a93d4ea commit 2c5578f
Show file tree
Hide file tree
Showing 21 changed files with 282 additions and 71 deletions.
2 changes: 1 addition & 1 deletion Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pipeline {
RUN_PORT=8081
POM_VERSION="0.2"
SERVICE_NAME="wirerest"
RUN_ARGS="--spring.profiles.active=${RUN_PROFILES} --server.port=${RUN_PORT}"
RUN_ARGS="--spring.profiles.active=${RUN_PROFILES} --server.port=${RUN_PORT} --debug"
}
stages {
stage('Build') {
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/com/wireguard/api/GlobalExceptionHandler.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.wireguard.api;


import com.wireguard.external.network.AlreadyUsedException;
import com.wireguard.external.network.NoFreeIpException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.server.ServerWebInputException;

@ControllerAdvice
public class GlobalExceptionHandler {
Expand All @@ -25,11 +28,29 @@ public ResponseEntity<AppError> catchException(Exception e) {
e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
}


@ExceptionHandler

public ResponseEntity<AppError> badRequestException(BadRequestException e) {
logger.error(e.getMessage(), e);
return new ResponseEntity<>(
new AppError(HttpStatus.BAD_REQUEST.value(),
e.getMessage()), HttpStatus.BAD_REQUEST);
}

@ExceptionHandler
public ResponseEntity<AppError> serverWebInputException(ServerWebInputException e) {
logger.warn("ServerWebInputException: ".formatted(e.getCause().getMessage()));
return new ResponseEntity<>(
new AppError(e.getStatusCode().value(),
e.getCause().getMessage()), e.getStatusCode());
}

@ExceptionHandler
public ResponseEntity<AppError> alreadyUsed(AlreadyUsedException e) {
logger.error(e.getMessage(), e);
return new ResponseEntity<>(
new AppError(HttpStatus.BAD_REQUEST.value(),
e.getMessage()), HttpStatus.BAD_REQUEST);
}
}
41 changes: 36 additions & 5 deletions src/main/java/com/wireguard/api/peer/PeerController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.wireguard.api.AppError;
import com.wireguard.api.BadRequestException;
import com.wireguard.api.ResourceNotFoundException;
import com.wireguard.external.network.Subnet;
import com.wireguard.external.shell.CommandExecutionException;
import com.wireguard.external.wireguard.ParsingException;
import com.wireguard.external.wireguard.WgManager;
import com.wireguard.external.wireguard.dto.CreatedPeer;
Expand All @@ -20,8 +22,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;
import java.util.*;

@RestController
public class PeerController {
Expand Down Expand Up @@ -120,10 +121,40 @@ public WgPeerDTO getPeerByPublicKey(String publicKey) throws ParsingException {
),
@ApiResponse(responseCode = "500", description = "Internal Server Error",
content = { @Content(mediaType = "application/json",
schema = @Schema(implementation = AppError.class)) }) })
schema = @Schema(implementation = AppError.class)) }),
@ApiResponse(responseCode = "400", description = "Bad request (Invalid parameters values)",
content = { @Content(mediaType = "application/json",
schema = @Schema(implementation = AppError.class)) })})
@PostMapping("/peer/create")
public ResponseEntity<CreatedPeer> createPeer() {
return new ResponseEntity<>(wgManager.createPeer(), HttpStatus.CREATED);
@Parameter(name = "publicKey", description = "Public key of the peer (Will be generated if not provided)")
@Parameter(name = "presharedKey", description = "Preshared key or empty if no psk required (Will be generated if not provided)", allowEmptyValue = true)
@Parameter(name = "privateKey", description = "Private key of the peer " +
"(Will be generated if not provided. " +
"If provided public key, empty string will be returned)")
@Parameter(name = "address", description = "CIDR of new peer in wireguard network interface (Will be generated if not provided)", schema = @Schema(format = "CIDR"), allowEmptyValue = true)
@Parameter(name = "persistentKeepalive", description = "Persistent keepalive interval in seconds (0 if not provided)")
public ResponseEntity<CreatedPeer> createPeer(
@RequestParam(value = "publicKey", required = false ) String publicKey,
@RequestParam(value = "presharedKey", required = false ) String presharedKey,
@RequestParam(value = "privateKey", required = false ) String privateKey,
@RequestParam(value = "address", required = false) Set<Subnet> address,
@RequestParam(value = "persistentKeepalive", required = false ) Integer persistentKeepalive
) {
CreatedPeer createdPeer;
try {
createdPeer = wgManager.createPeerGenerateNulls(
publicKey,
presharedKey,
privateKey,
address,
persistentKeepalive
);
} catch (IllegalArgumentException | ParsingException e){
throw new BadRequestException(e.getMessage());
} catch (CommandExecutionException e){
throw new BadRequestException("Wireguard error: %s".formatted(e.getStderr().strip()));
}
return new ResponseEntity<>(createdPeer, HttpStatus.CREATED);
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.wireguard.external.network;

public class AlreadyUsedException extends RuntimeException{
public AlreadyUsedException(String message){
super(message);
}
}
45 changes: 29 additions & 16 deletions src/main/java/com/wireguard/external/network/IpResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import org.apache.commons.lang3.tuple.ImmutablePair;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.*;


public class IpResolver {
Expand All @@ -33,23 +31,38 @@ public IpResolver(Subnet allowedIpsSubnet){
}

public Subnet takeFreeSubnet(Integer mask){
ImmutablePair<Subnet, IpRange> subnetAndRange = findFreeSubnetAndRange(mask);
if (subnetAndRange != null){
Subnet requestedSubnet = subnetAndRange.getLeft();
int requestedSubnetIndex = calculatePreviousRangeIndex(findFirstGreater(requestedSubnet.getFirstIpNumeric()));
availableRanges.remove(requestedSubnetIndex);
availableRanges.addAll(requestedSubnetIndex, insertSubnetIntoIpRange(requestedSubnet, subnetAndRange.getRight()));
availableIpsCount -= requestedSubnet.getIpCount();
return requestedSubnet;
} else {
throw new NoFreeIpException("Cannot find free subnet with mask " + mask);
}
}

public Subnet findFreeSubnet(Integer mask){
ImmutablePair<Subnet, IpRange> result = findFreeSubnetAndRange(mask);
return result==null ? null : result.getLeft();
}

private ImmutablePair<Subnet, IpRange> findFreeSubnetAndRange(Integer mask){
long addressRequestCount = (long) Math.pow(2, 32-mask);
for (int i = 0; i < availableRanges.size(); i++){
IpRange range = availableRanges.get(i);
for (IpRange range : availableRanges) {
long firstAddress = Math.ceilDiv(range.getLeast(), addressRequestCount);
long addressNeeded = firstAddress*addressRequestCount-range.getLeast()+addressRequestCount;
if (addressNeeded > range.getIpsCount()){
long addressNeeded = firstAddress * addressRequestCount - range.getLeast() + addressRequestCount;
if (addressNeeded > range.getIpsCount()) {
continue;
}
Subnet givenSubnet = new Subnet(
numericIpToByte(firstAddress*addressRequestCount),
Subnet subnet = new Subnet(
numericIpToByte(firstAddress * addressRequestCount),
mask);
availableRanges.remove(i);
availableRanges.addAll(i, insertSubnetIntoIpRange(givenSubnet, range));
availableIpsCount -= givenSubnet.getIpCount();
return givenSubnet;
return new ImmutablePair<>(subnet, range);
}
throw new NoFreeIpException("Cannot find free subnet with mask "+mask);
return null;
}

public void takeIp(String ip){
Expand All @@ -74,7 +87,7 @@ public void takeSubnet(Subnet subnet){
availableRanges.addAll(currRangeIndex, insertSubnetIntoIpRange(subnet, availableRange));
availableIpsCount -= subnet.getIpCount();
} else {
throw new NoFreeIpException("Subnet " + subnet + " is is already used");
throw new AlreadyUsedException("Subnet " + subnet + " is is already used");
}
}

Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/wireguard/external/network/Subnet.java
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,5 @@ private static byte[] parseMask(int mask){
public int compareTo(Subnet o) {
return Long.compare(getFirstIpNumeric(), o.getFirstIpNumeric());
}

}
19 changes: 19 additions & 0 deletions src/main/java/com/wireguard/external/network/SubnetFormatter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.wireguard.external.network;
import org.springframework.format.Formatter;
import org.springframework.stereotype.Component;
import java.util.Locale;

@Component
public class SubnetFormatter implements Formatter<Subnet> {


@Override
public Subnet parse(String text, Locale locale) {
return Subnet.valueOf(text);
}

@Override
public String print(Subnet object, Locale locale) {
return object.toString();
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
package com.wireguard.external.shell;

import lombok.Data;

@Data
public class CommandExecutionException extends RuntimeException {
private final String stderr;
private final String stdout;
private final int exitCode;
public CommandExecutionException(String command, int exitCode, String stdout, String stderr) {
super("Error executing command: %s, exit code: %d, stdout: %s, stderr: %s".formatted(
command,
exitCode,
stdout,
stderr));
this.exitCode = exitCode;
this.stderr = stderr;
this.stdout = stdout;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.wireguard.external.shell;

import com.wireguard.converters.StreamToStringConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -33,6 +32,7 @@ public String execute(String[] command, List<Integer> allowedExitCodes) {
if (!allowedExitCodes.contains(exitCode)) {
throw new CommandExecutionException(String.join(" ", command), exitCode, stdout, stderr);
}
logger.debug("Command executed successfully: %s".formatted(String.join(" ", command)));
return stdout;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.wireguard.converters;
package com.wireguard.external.shell;

import com.wireguard.external.shell.ShellRunner;
import jakarta.annotation.Nullable;
Expand Down
49 changes: 32 additions & 17 deletions src/main/java/com/wireguard/external/wireguard/WgManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import com.wireguard.external.network.IpResolver;
import com.wireguard.external.network.NetworkInterfaceDTO;
import com.wireguard.external.network.Subnet;
import com.wireguard.external.shell.CommandExecutionException;
import com.wireguard.external.shell.ShellRunner;
import com.wireguard.external.wireguard.converters.WgPeerContainerToWgPeerDTOSet;
import com.wireguard.external.wireguard.converters.WgPeerIterableToWgPeerDTOList;
import com.wireguard.external.wireguard.dto.CreatedPeer;
import com.wireguard.external.wireguard.dto.WgInterfaceDTO;
import com.wireguard.external.wireguard.dto.WgPeerDTO;
import jakarta.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -22,15 +24,18 @@
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

@Component
@Scope("singleton")
public class WgManager {

private static final Logger logger = LoggerFactory.getLogger(ShellRunner.class);
private final NetworkInterfaceDTO wgInterface;
@Value("${wg.interface.new_client_subnet_mask}")
private int defaultMaskForNewClients = 32;
@Value("${wg.interface.default.mask}")
private static int DEFAULT_MASK_FOR_NEW_CLIENTS = 32;
@Value("${wg.interface.default.persistent_keepalive}")
private static int DEFAULT_PERSISTENT_KEEPALIVE = 0;
private final IpResolver wgIpResolver;
private static WgTool wgTool;
WgPeerContainerToWgPeerDTOSet containerToPeer = new WgPeerContainerToWgPeerDTOSet();
Expand Down Expand Up @@ -88,34 +93,44 @@ public List<WgPeerDTO> getPeers(Sort sort){
return iterableToPeer.convert(peerContainer.findAll(sort));
}

public List<WgPeerDTO> getPeers(Pageable pageable){
public List<WgPeerDTO> getPeers(Pageable pageable) {
WgPeerContainer peerContainer = getWgPeerContainer();
Page<WgPeer> page = peerContainer.findAll(pageable);
return iterableToPeer.convert(page);
}


public CreatedPeer createPeer(){
String privateKey = wgTool.generatePrivateKey();
String publicKey = wgTool.generatePublicKey(privateKey);
String presharedKey = wgTool.generatePresharedKey();
Subnet address = wgIpResolver.takeFreeSubnet(defaultMaskForNewClients);
CreatedPeer createdPeer = new CreatedPeer(
publicKey,
presharedKey,
privateKey,
Set.of(address.toString()),
0);
public CreatedPeer createPeer(String privateKey, String publicKey, String presharedKey, Set<Subnet> allowedIps, int persistentKeepalive) {
CreatedPeer createdPeer = new CreatedPeer(publicKey, presharedKey, privateKey,
allowedIps.stream().map(Subnet::toString).collect(Collectors.toSet()),
persistentKeepalive);
logger.debug("Creating peer: %s".formatted(createdPeer.toString()));
allowedIps.forEach(wgIpResolver::takeSubnet);
try {
wgTool.addPeer(wgInterface.getName(), createdPeer);
logger.info("Created peer, public key: %s".formatted(publicKey.substring(0, 6)));
} catch (Exception e){
wgIpResolver.freeSubnet(address);
}catch (CommandExecutionException e) {
allowedIps.forEach(wgIpResolver::freeSubnet);
throw e;
}
logger.info("Created peer, public key: %s".formatted(publicKey.substring(0, 6)));
return createdPeer;
}

public CreatedPeer createPeerGenerateNulls(@Nullable String publicKey, @Nullable String presharedKey,
@Nullable String privateKey, @Nullable Set<Subnet> allowedIps,
@Nullable Integer persistentKeepalive){
privateKey = privateKey == null ? wgTool.generatePrivateKey() : privateKey;
publicKey = publicKey == null ? wgTool.generatePublicKey(privateKey) : publicKey;
presharedKey = presharedKey == null ? wgTool.generatePresharedKey() : presharedKey;
persistentKeepalive = persistentKeepalive == null ? DEFAULT_PERSISTENT_KEEPALIVE : persistentKeepalive;
allowedIps = allowedIps == null ? Set.of(wgIpResolver.findFreeSubnet(DEFAULT_MASK_FOR_NEW_CLIENTS)) : allowedIps;
return createPeer(privateKey, publicKey, presharedKey, allowedIps, persistentKeepalive);
}

public CreatedPeer createPeer(){
return createPeerGenerateNulls(null, null, null, null, null);
}

public void deletePeer(String publicKey) {

wgTool.deletePeer(wgInterface.getName(), publicKey);
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/com/wireguard/external/wireguard/WgPeerBuilder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.wireguard.external.wireguard;

import com.wireguard.external.network.IpResolver;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class WgPeerBuilder {

@Value("${wg.interface.new_client_subnet_mask}")
private static int DEFAULT_MASK;
private IpResolver wgIpResolver;

public WgPeerBuilder(IpResolver wgIpResolver) {
this.wgIpResolver = wgIpResolver;
}
}
Loading

0 comments on commit 2c5578f

Please sign in to comment.