diff options
author | Trumeet <yuuta@yuuta.moe> | 2021-04-03 15:17:08 -0700 |
---|---|---|
committer | Trumeet <yuuta@yuuta.moe> | 2021-04-03 15:17:08 -0700 |
commit | fb9fb86002178f9e8b4693d2cf50cacb55978424 (patch) | |
tree | 605d1efdcada4f81a0d1f0126d71199ce9b2a972 | |
parent | 8b655e6058feaa4a00c6824400d57b3969d5e4db (diff) | |
download | dn42peering-fb9fb86002178f9e8b4693d2cf50cacb55978424.tar dn42peering-fb9fb86002178f9e8b4693d2cf50cacb55978424.tar.gz dn42peering-fb9fb86002178f9e8b4693d2cf50cacb55978424.tar.bz2 dn42peering-fb9fb86002178f9e8b4693d2cf50cacb55978424.zip |
feat(agent): use `ip` to operate WireGuard interfaces instead of wg-quick
Existing wg-quick services will be automatically removed. Manual inspection may be required.
17 files changed, 1150 insertions, 136 deletions
diff --git a/agent/build.gradle b/agent/build.gradle index ccd5471..67be7e2 100644 --- a/agent/build.gradle +++ b/agent/build.gradle @@ -34,6 +34,8 @@ dependencies { annotationProcessor "io.vertx:vertx-service-proxy:${project.vertxVersion}" compileOnly "io.vertx:vertx-codegen:${project.vertxVersion}" + implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.2' + implementation project(':rpc-common') } diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/grpc/AgentServiceImpl.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/grpc/AgentServiceImpl.java index c1ad43d..4c4deb2 100644 --- a/agent/src/main/java/moe/yuuta/dn42peering/agent/grpc/AgentServiceImpl.java +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/grpc/AgentServiceImpl.java @@ -44,7 +44,8 @@ class AgentServiceImpl extends VertxAgentGrpc.AgentVertxImplBase { } return CompositeFuture.all(changes); }) - .onComplete(res -> logger.info("Deployment finished. Detailed log can be traced above.")) + .onSuccess(res -> logger.info("Deployment finished. Detailed log can be traced above.")) + .onFailure(err -> logger.error("Deployment failed. Detailed log can be traced above.", err)) .compose(compositeFuture -> Future.succeededFuture(null)); } } diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/AddrInfoItem.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/AddrInfoItem.java new file mode 100644 index 0000000..62017b2 --- /dev/null +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/AddrInfoItem.java @@ -0,0 +1,96 @@ +package moe.yuuta.dn42peering.agent.ip; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class AddrInfoItem{ + + @JsonProperty("address") + private String address; + + @JsonProperty("scope") + private String scope; + + @JsonProperty("prefixlen") + private Integer prefixlen; + + @JsonProperty("valid_life_time") + private Long validLifeTime; + + @JsonProperty("family") + private String family; + + @JsonProperty("preferred_life_time") + private Long preferredLifeTime; + + @JsonProperty("local") + private String local; + + @JsonProperty("label") + private String label; + + public void setAddress(String address){ + this.address = address; + } + + public String getAddress(){ + return address; + } + + public void setScope(String scope){ + this.scope = scope; + } + + public String getScope(){ + return scope; + } + + public void setPrefixlen(Integer prefixlen){ + this.prefixlen = prefixlen; + } + + public Integer getPrefixlen(){ + return prefixlen; + } + + public void setValidLifeTime(Long validLifeTime){ + this.validLifeTime = validLifeTime; + } + + public Long getValidLifeTime(){ + return validLifeTime; + } + + public void setFamily(String family){ + this.family = family; + } + + public String getFamily(){ + return family; + } + + public void setPreferredLifeTime(Long preferredLifeTime){ + this.preferredLifeTime = preferredLifeTime; + } + + public Long getPreferredLifeTime(){ + return preferredLifeTime; + } + + public void setLocal(String local){ + this.local = local; + } + + public String getLocal(){ + return local; + } + + public void setLabel(String label){ + this.label = label; + } + + public String getLabel(){ + return label; + } +}
\ No newline at end of file diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/Address.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/Address.java new file mode 100644 index 0000000..c55e39f --- /dev/null +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/Address.java @@ -0,0 +1,208 @@ +package moe.yuuta.dn42peering.agent.ip; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class Address{ + + @JsonProperty("qdisc") + private String qdisc; + + @JsonProperty("ifindex") + private Integer ifindex; + + @JsonProperty("max_mtu") + private Integer maxMtu; + + @JsonProperty("flags") + private List<String> flags; + + @JsonProperty("txqlen") + private Integer txqlen; + + @JsonProperty("num_rx_queues") + private Integer numRxQueues; + + @JsonProperty("min_mtu") + private Integer minMtu; + + @JsonProperty("mtu") + private Integer mtu; + + @JsonProperty("link_type") + private String linkType; + + @JsonProperty("gso_max_segs") + private Integer gsoMaxSegs; + + @JsonProperty("ifname") + private String ifname; + + @JsonProperty("num_tx_queues") + private Integer numTxQueues; + + @JsonProperty("promiscuity") + private Integer promiscuity; + + @JsonProperty("operstate") + private String operstate; + + @JsonProperty("addr_info") + private List<AddrInfoItem> addrInfo; + + @JsonProperty("gso_max_size") + private Integer gsoMaxSize; + + @JsonProperty("group") + private String group; + + @JsonProperty("linkinfo") + private Linkinfo linkinfo; + + public void setQdisc(String qdisc){ + this.qdisc = qdisc; + } + + public String getQdisc(){ + return qdisc; + } + + public void setIfindex(Integer ifindex){ + this.ifindex = ifindex; + } + + public Integer getIfindex(){ + return ifindex; + } + + public void setMaxMtu(Integer maxMtu){ + this.maxMtu = maxMtu; + } + + public Integer getMaxMtu(){ + return maxMtu; + } + + public void setFlags(List<String> flags){ + this.flags = flags; + } + + public List<String> getFlags(){ + return flags; + } + + public void setTxqlen(Integer txqlen){ + this.txqlen = txqlen; + } + + public Integer getTxqlen(){ + return txqlen; + } + + public void setNumRxQueues(Integer numRxQueues){ + this.numRxQueues = numRxQueues; + } + + public Integer getNumRxQueues(){ + return numRxQueues; + } + + public void setMinMtu(Integer minMtu){ + this.minMtu = minMtu; + } + + public Integer getMinMtu(){ + return minMtu; + } + + public void setMtu(Integer mtu){ + this.mtu = mtu; + } + + public Integer getMtu(){ + return mtu; + } + + public void setLinkType(String linkType){ + this.linkType = linkType; + } + + public String getLinkType(){ + return linkType; + } + + public void setGsoMaxSegs(Integer gsoMaxSegs){ + this.gsoMaxSegs = gsoMaxSegs; + } + + public Integer getGsoMaxSegs(){ + return gsoMaxSegs; + } + + public void setIfname(String ifname){ + this.ifname = ifname; + } + + public String getIfname(){ + return ifname; + } + + public void setNumTxQueues(Integer numTxQueues){ + this.numTxQueues = numTxQueues; + } + + public Integer getNumTxQueues(){ + return numTxQueues; + } + + public void setPromiscuity(Integer promiscuity){ + this.promiscuity = promiscuity; + } + + public Integer getPromiscuity(){ + return promiscuity; + } + + public void setOperstate(String operstate){ + this.operstate = operstate; + } + + public String getOperstate(){ + return operstate; + } + + public void setAddrInfo(List<AddrInfoItem> addrInfo){ + this.addrInfo = addrInfo; + } + + public List<AddrInfoItem> getAddrInfo(){ + return addrInfo; + } + + public void setGsoMaxSize(Integer gsoMaxSize){ + this.gsoMaxSize = gsoMaxSize; + } + + public Integer getGsoMaxSize(){ + return gsoMaxSize; + } + + public void setGroup(String group){ + this.group = group; + } + + public String getGroup(){ + return group; + } + + public void setLinkinfo(Linkinfo linkinfo){ + this.linkinfo = linkinfo; + } + + public Linkinfo getLinkinfo(){ + return linkinfo; + } +}
\ No newline at end of file diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/IP.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/IP.java new file mode 100644 index 0000000..109f206 --- /dev/null +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/IP.java @@ -0,0 +1,216 @@ +package moe.yuuta.dn42peering.agent.ip; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import moe.yuuta.dn42peering.agent.IOUtils; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class IP { + private static Logger logger = LoggerFactory.getLogger(IP.class.getSimpleName()); + + public static Future<String> batch(@Nonnull Vertx vertx, + @Nonnull IPOptions options, + @Nonnull List<String> commands) { + logger.info("Running batch ip commands:\n" + + String.join("\n", commands)); + return vertx.executeBlocking(f -> { + final List<String> cmds = new ArrayList<>(); + cmds.add("ip"); + cmds.addAll(options.toCommand()); + cmds.add("-b"); + cmds.add("/dev/stdin"); + logger.info("Executing " + cmds); + final ProcessBuilder builder = new ProcessBuilder() + .command(cmds.toArray(new String[]{})); + builder.environment().put("LANG", "C"); + + try { + final Process process = builder.start(); + final OutputStream stdin = process.getOutputStream(); + final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stdin)); + writer.write(String.join("\n", commands)); + writer.close(); + final InputStream stdout = process.getInputStream(); + final InputStream stderr = process.getErrorStream(); + final int res = process.waitFor(); + switch (res) { + case 0: + f.complete(IOUtils.read(stdout)); + break; + case 1: + f.fail(new IPSyntaxException(IOUtils.read(stderr))); + break; + case 2: + f.fail(new IPKernelException(IOUtils.read(stderr))); + break; + default: + f.fail(new IPException(res, IOUtils.read(stderr))); + break; + } + } catch (IOException | InterruptedException e) { + f.fail(e); + } + }); + } + + @Nonnull + private static List<String> generateScript(@Nonnull String object, + @Nonnull String action, + @Nullable String... arguments) { + final List<String> cmds = new ArrayList<>(); + cmds.add(object); + cmds.add(action); + if (arguments != null) cmds.addAll(Arrays.asList(arguments)); + return cmds; + } + + @Nonnull + public static Future<String> ip(@Nonnull Vertx vertx, + @Nonnull IPOptions options, + @Nonnull List<String> cmd) { + return vertx.executeBlocking(f -> { + final List<String> cmds = new ArrayList<>(); + cmds.add("ip"); + cmds.addAll(options.toCommand()); + cmds.addAll(cmd); + logger.info("Executing '" + cmds + "'."); + final ProcessBuilder builder = new ProcessBuilder() + .command(cmds.toArray(new String[]{})); + builder.environment().put("LANG", "C"); + + try { + final Process process = builder.start(); + final InputStream stdout = process.getInputStream(); + final InputStream stderr = process.getErrorStream(); + final int res = process.waitFor(); + switch (res) { + case 0: + f.complete(IOUtils.read(stdout)); + break; + case 1: + f.fail(new IPSyntaxException(IOUtils.read(stderr))); + break; + case 2: + f.fail(new IPKernelException(IOUtils.read(stderr))); + break; + default: + f.fail(new IPException(res, IOUtils.read(stderr))); + break; + } + } catch (IOException | InterruptedException e) { + f.fail(e); + } + }); + } + + @Nonnull + public static Future<String> ip(@Nonnull Vertx vertx, + @Nonnull IPOptions options, + @Nonnull String object, + @Nonnull String action, + @Nullable String... arguments) { + return ip(vertx, options, generateScript(object, action, arguments)); + } + + public static class Link { + private static final String OBJECT = "link"; + + public static Future<List<moe.yuuta.dn42peering.agent.ip.Link>> handler(@Nonnull String rawJson) { + final JsonArray arr = new JsonArray(rawJson); + return Future.succeededFuture( + arr.stream() + .map(obj -> ((JsonObject) obj).mapTo(moe.yuuta.dn42peering.agent.ip.Link.class)) + .collect(Collectors.toList())); + } + + @Nonnull + public static List<String> show(@Nullable String device) { + return generateScript(OBJECT, + "show", + device); + } + + + @Nonnull + public static List<String> add(@Nonnull String device, + @Nonnull String type) { + return generateScript(OBJECT, + "add", + "dev", device, + "type", type); + } + + @Nonnull + public static List<String> set(@Nonnull String device, + @Nonnull String... statements) { + final String[] arguments = new String[statements.length + 1]; + arguments[0] = device; + System.arraycopy(statements, 0, arguments, 1, statements.length); + return generateScript(OBJECT, + "set", + arguments); + } + + @Nonnull + public static List<String> del(@Nonnull String device) { + return generateScript(OBJECT, + "del", + "dev", device); + } + } + + public static class Addr { + private static final String OBJECT = "addr"; + + @Nonnull + public static Future<List<Address>> handler(@Nonnull String rawJson) { + final JsonArray arr = new JsonArray(rawJson); + return Future.succeededFuture( + arr.stream() + .map(obj -> ((JsonObject) obj).mapTo(Address.class)) + .collect(Collectors.toList())); + } + + @Nonnull + public static List<String> show(@Nullable String device) { + return generateScript(OBJECT, + "show", + device == null ? null : new String[] { device }); + } + + @Nonnull + public static List<String> add(@Nonnull String ifaddr, + @Nonnull String device, + @Nullable String peer) { + String[] args = new String[peer == null ? 3 : 5]; + args[0] = ifaddr; + args[1] = "dev"; + args[2] = device; + if (peer != null) { + args[3] = "peer"; + args[4] = peer; + } + return generateScript(OBJECT, + "add", + args); + } + + @Nonnull + public static List<String> flush(@Nullable String device) { + return generateScript(OBJECT, + "flush", + device); + } + } +} diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/IPException.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/IPException.java new file mode 100644 index 0000000..0649a01 --- /dev/null +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/IPException.java @@ -0,0 +1,12 @@ +package moe.yuuta.dn42peering.agent.ip; + +public class IPException extends Exception { + public final int returnCode; + public final String stderr; + + public IPException(int returnCode, String stderr) { + super(String.format("Failed to execute ip: %s", stderr)); + this.returnCode = returnCode; + this.stderr = stderr; + } +} diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/IPKernelException.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/IPKernelException.java new file mode 100644 index 0000000..ec2b94f --- /dev/null +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/IPKernelException.java @@ -0,0 +1,7 @@ +package moe.yuuta.dn42peering.agent.ip; + +public class IPKernelException extends IPException { + public IPKernelException(String stderr) { + super(2, stderr); + } +} diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/IPOptions.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/IPOptions.java new file mode 100644 index 0000000..45f3a5d --- /dev/null +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/IPOptions.java @@ -0,0 +1,82 @@ +package moe.yuuta.dn42peering.agent.ip; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class IPOptions { + public enum Family { + INET, + INET6, + BRIDGE, + MPLS, + LINK + } + + private boolean details; + private Family family; + private String netns; + private boolean force; + + public boolean isDetails() { + return details; + } + + @Nonnull + public IPOptions setDetails(boolean details) { + this.details = details; + return this; + } + + public Family getFamily() { + return family; + } + + @Nonnull + public IPOptions setFamily(Family family) { + this.family = family; + return this; + } + + public String getNetns() { + return netns; + } + + @Nonnull + public IPOptions setNetns(String netns) { + this.netns = netns; + return this; + } + + public boolean isForce() { + return force; + } + + @Nonnull + public IPOptions setForce(boolean force) { + this.force = force; + return this; + } + + @Nonnull + List<String> toCommand() { + final List<String> cmds = new ArrayList<>(); + if(details) + cmds.add("-details"); + if(family != null) { + cmds.add("-family"); + cmds.add(family.toString().toLowerCase(Locale.ROOT)); + } + if(netns != null) { + cmds.add("-netns"); + cmds.add(netns); + } + if(force) { + cmds.add("-force"); + } + cmds.add("-json"); + cmds.add("-c=never"); + return cmds; + } +} diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/IPSyntaxException.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/IPSyntaxException.java new file mode 100644 index 0000000..3d8d9db --- /dev/null +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/IPSyntaxException.java @@ -0,0 +1,7 @@ +package moe.yuuta.dn42peering.agent.ip; + +public class IPSyntaxException extends IPException { + public IPSyntaxException(String stderr) { + super(1, stderr); + } +} diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/Link.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/Link.java new file mode 100644 index 0000000..c27cd54 --- /dev/null +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/Link.java @@ -0,0 +1,219 @@ +package moe.yuuta.dn42peering.agent.ip; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class Link{ + + @JsonProperty("qdisc") + private String qdisc; + + @JsonProperty("ifindex") + private Integer ifindex; + + @JsonProperty("linkmode") + private String linkmode; + + @JsonProperty("max_mtu") + private Integer maxMtu; + + @JsonProperty("flags") + private List<String> flags; + + @JsonProperty("txqlen") + private Integer txqlen; + + @JsonProperty("num_rx_queues") + private Integer numRxQueues; + + @JsonProperty("inet6_addr_gen_mode") + private String inet6AddrGenMode; + + @JsonProperty("min_mtu") + private Integer minMtu; + + @JsonProperty("mtu") + private Integer mtu; + + @JsonProperty("link_type") + private String linkType; + + @JsonProperty("gso_max_segs") + private Integer gsoMaxSegs; + + @JsonProperty("ifname") + private String ifname; + + @JsonProperty("num_tx_queues") + private Integer numTxQueues; + + @JsonProperty("promiscuity") + private Integer promiscuity; + + @JsonProperty("operstate") + private String operstate; + + @JsonProperty("gso_max_size") + private Integer gsoMaxSize; + + @JsonProperty("group") + private String group; + + @JsonProperty("linkinfo") + private Linkinfo linkinfo; + + public void setQdisc(String qdisc){ + this.qdisc = qdisc; + } + + public String getQdisc(){ + return qdisc; + } + + public void setIfindex(Integer ifindex){ + this.ifindex = ifindex; + } + + public Integer getIfindex(){ + return ifindex; + } + + public void setLinkmode(String linkmode){ + this.linkmode = linkmode; + } + + public String getLinkmode(){ + return linkmode; + } + + public void setMaxMtu(Integer maxMtu){ + this.maxMtu = maxMtu; + } + + public Integer getMaxMtu(){ + return maxMtu; + } + + public void setFlags(List<String> flags){ + this.flags = flags; + } + + public List<String> getFlags(){ + return flags; + } + + public void setTxqlen(Integer txqlen){ + this.txqlen = txqlen; + } + + public Integer getTxqlen(){ + return txqlen; + } + + public void setNumRxQueues(Integer numRxQueues){ + this.numRxQueues = numRxQueues; + } + + public Integer getNumRxQueues(){ + return numRxQueues; + } + + public void setInet6AddrGenMode(String inet6AddrGenMode){ + this.inet6AddrGenMode = inet6AddrGenMode; + } + + public String getInet6AddrGenMode(){ + return inet6AddrGenMode; + } + + public void setMinMtu(Integer minMtu){ + this.minMtu = minMtu; + } + + public Integer getMinMtu(){ + return minMtu; + } + + public void setMtu(Integer mtu){ + this.mtu = mtu; + } + + public Integer getMtu(){ + return mtu; + } + + public void setLinkType(String linkType){ + this.linkType = linkType; + } + + public String getLinkType(){ + return linkType; + } + + public void setGsoMaxSegs(Integer gsoMaxSegs){ + this.gsoMaxSegs = gsoMaxSegs; + } + + public Integer getGsoMaxSegs(){ + return gsoMaxSegs; + } + + public void setIfname(String ifname){ + this.ifname = ifname; + } + + public String getIfname(){ + return ifname; + } + + public void setNumTxQueues(Integer numTxQueues){ + this.numTxQueues = numTxQueues; + } + + public Integer getNumTxQueues(){ + return numTxQueues; + } + + public void setPromiscuity(Integer promiscuity){ + this.promiscuity = promiscuity; + } + + public Integer getPromiscuity(){ + return promiscuity; + } + + public void setOperstate(String operstate){ + this.operstate = operstate; + } + + public String getOperstate(){ + return operstate; + } + + public void setGsoMaxSize(Integer gsoMaxSize){ + this.gsoMaxSize = gsoMaxSize; + } + + public Integer getGsoMaxSize(){ + return gsoMaxSize; + } + + public void setGroup(String group){ + this.group = group; + } + + public String getGroup(){ + return group; + } + + public void setLinkinfo(Linkinfo linkinfo){ + this.linkinfo = linkinfo; + } + + public Linkinfo getLinkinfo(){ + return linkinfo; + } +}
\ No newline at end of file diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/Linkinfo.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/Linkinfo.java new file mode 100644 index 0000000..26b0875 --- /dev/null +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/ip/Linkinfo.java @@ -0,0 +1,19 @@ +package moe.yuuta.dn42peering.agent.ip; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class Linkinfo{ + + @JsonProperty("info_kind") + private String infoKind; + + public void setInfoKind(String infoKind){ + this.infoKind = infoKind; + } + + public String getInfoKind(){ + return infoKind; + } +}
\ No newline at end of file diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/FileChange.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/FileChange.java index 146e41c..5677ba1 100644 --- a/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/FileChange.java +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/FileChange.java @@ -31,7 +31,7 @@ public class FileChange extends Change { public Future<Void> execute(@Nonnull Vertx vertx) { switch (Action.valueOf(action)) { case CREATE_AND_WRITE: - logger.info("Writing " + id + " with:\n" + to); + logger.info("Writing " + id); return vertx.fileSystem().open(id, new OpenOptions() .setCreateNew(true) .setTruncateExisting(true) @@ -44,7 +44,7 @@ public class FileChange extends Change { return asyncFile.close(); }); case OVERWRITE: - logger.info("Overwriting " + id + " with:\n" + to); + logger.info("Overwriting " + id); return vertx.fileSystem().open(id, new OpenOptions() .setCreateNew(false) .setTruncateExisting(true) diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/IPChange.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/IPChange.java new file mode 100644 index 0000000..bc9c54d --- /dev/null +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/IPChange.java @@ -0,0 +1,31 @@ +package moe.yuuta.dn42peering.agent.provision; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import moe.yuuta.dn42peering.agent.ip.IP; +import moe.yuuta.dn42peering.agent.ip.IPOptions; + +import javax.annotation.Nonnull; +import java.util.List; + +public class IPChange extends Change { + private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName()); + + private final boolean force; + private final List<String> commands; + + public IPChange(boolean force, @Nonnull List<String> commands) { + super("ip", null, "exec"); + this.force = force; + this.commands = commands; + } + + @Nonnull + @Override + public Future<Void> execute(@Nonnull Vertx vertx) { + return IP.batch(vertx, new IPOptions().setForce(force), commands) + .compose(stdout -> Future.succeededFuture()); + } +} diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/WireGuardProvisioner.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/WireGuardProvisioner.java index fbdbd3f..afdfae9 100644 --- a/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/WireGuardProvisioner.java +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/WireGuardProvisioner.java @@ -4,19 +4,22 @@ import io.vertx.core.CompositeFuture; import io.vertx.core.Future; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; -import io.vertx.core.file.FileSystemException; import io.vertx.core.impl.logging.Logger; import io.vertx.core.impl.logging.LoggerFactory; import io.vertx.ext.web.common.template.TemplateEngine; import io.vertx.ext.web.templ.freemarker.FreeMarkerTemplateEngine; +import moe.yuuta.dn42peering.agent.ip.AddrInfoItem; +import moe.yuuta.dn42peering.agent.ip.Address; +import moe.yuuta.dn42peering.agent.ip.IP; +import moe.yuuta.dn42peering.agent.ip.IPOptions; import moe.yuuta.dn42peering.agent.proto.Node; import moe.yuuta.dn42peering.agent.proto.WireGuardConfig; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.File; import java.io.IOException; import java.net.Inet6Address; -import java.nio.file.NoSuchFileException; import java.util.*; import java.util.stream.Collectors; @@ -31,7 +34,7 @@ public class WireGuardProvisioner implements IProvisioner<WireGuardConfig> { } public WireGuardProvisioner(@Nonnull TemplateEngine engine, - @Nonnull Vertx vertx) { + @Nonnull Vertx vertx) { this.engine = engine; this.vertx = vertx; } @@ -42,28 +45,10 @@ public class WireGuardProvisioner implements IProvisioner<WireGuardConfig> { final List<String> actualNames = Arrays.stream(actualNamesRaw == null ? new String[]{} : actualNamesRaw) .sorted() .collect(Collectors.toList()); - final String[] desiredNames = allDesired - .stream() - .map(desired -> generateWGPath(desired.getInterface())) - .sorted() - .collect(Collectors.toList()) - .toArray(new String[]{}); - final List<Integer> toRemove = new ArrayList<>(actualNames.size()); - for (int i = 0; i < desiredNames.length; i ++) { - toRemove.clear(); - for(int j = 0; j < actualNames.size(); j ++) { - if(("/etc/wireguard/" + actualNames.get(j)).equals(desiredNames[i])) { - toRemove.add(j); - } - } - for (int j = 0; j < toRemove.size(); j ++) { - actualNames.remove(toRemove.get(j).intValue()); - } - } return Future.succeededFuture(actualNames.stream() .flatMap(string -> { - return Arrays.stream(new Change[] { - new CommandChange(new String[]{ "systemctl", "disable", "--now", "-q", "wg-quick@" + string }), + return Arrays.stream(new Change[]{ + new CommandChange(new String[]{"systemctl", "disable", "--now", "-q", "wg-quick@" + string}), new FileChange("/etc/wireguard/" + string, null, FileChange.Action.DELETE.toString()) }); }) @@ -71,129 +56,213 @@ public class WireGuardProvisioner implements IProvisioner<WireGuardConfig> { } @Nonnull - private static String generateWGPath(@Nonnull String iif) { - return String.format("/etc/wireguard/%s.conf", iif); + private Future<Buffer> renderConfig(@Nonnull WireGuardConfig config) { + final Map<String, Object> params = new HashMap<>(5); + params.put("listen_port", config.getListenPort()); + params.put("self_priv_key", config.getSelfPrivKey()); + params.put("preshared_key", config.getSelfPresharedSecret()); + if (!config.getEndpoint().equals("")) { + params.put("endpoint", config.getEndpoint()); + } + params.put("peer_pub_key", config.getPeerPubKey()); + + return engine.render(params, "wg.conf.ftlh"); + } + + @Nullable + private WireGuardConfig searchDesiredConfig(@Nonnull List<WireGuardConfig> configs, + @Nonnull String device) { + // TODO: Optimize algorithm + for (final WireGuardConfig config : configs) { + if(config.getInterface().equals(device)) + return config; + } + return null; + } + + @Nullable + private Address searchActualAddress(@Nonnull List<Address> addresses, + @Nonnull String device) { + // TODO: Optimize algorithm + for (final Address address : addresses) { + if(address.getIfname().equals(device)) + return address; + } + return null; } @Nonnull - private Future<Buffer> readConfig(@Nonnull String iif) { - return Future.future(f -> { - vertx.fileSystem() - .readFile(generateWGPath(iif)) - .onFailure(err -> { - if(err instanceof FileSystemException && - err.getCause() instanceof NoSuchFileException) { - f.complete(null); + private List<String> calculateSingleNetlinkChanges(@Nonnull Node node, + @Nonnull WireGuardConfig desired, + @Nullable Address actual) throws IOException { + final boolean linkLocal = + !desired.getPeerIPv6().isEmpty() && + Inet6Address.getByName(desired.getPeerIPv6()).isLinkLocalAddress(); + + final boolean desireIP6 = !desired.getPeerIPv6().isEmpty(); + final boolean needCreateInterface = actual == null; + final boolean needCreateAddrs; + final boolean needUp; + + if(actual == null) { + needCreateAddrs = true; + needUp = true; + } else { + needUp = !actual.getOperstate().equals("UP") && + !actual.getOperstate().equals("UNKNOWN"); + AddrInfoItem actualIP4 = null; + AddrInfoItem actualIP6 = null; + boolean excessiveIPs = false; + for (final AddrInfoItem item : actual.getAddrInfo()) { + switch (item.getFamily()) { + case "inet": + if(actualIP4 != null) { + excessiveIPs = true; + break; } else { - f.fail(err); + actualIP4 = item; } - }) - .onSuccess(f::complete); - }); - } + break; + case "inet6": + if(actualIP6 != null) { + excessiveIPs = true; + break; + } else { + actualIP6 = item; + } + break; + default: + excessiveIPs = true; + break; + } + } + if(excessiveIPs || actualIP4 == null || (desireIP6 && actualIP6 == null) || + (!desireIP6 && actualIP6 != null)) { + logger.info("Recreating addresses for " + desired.getId() + " since there are extra addresses or necessary addresses cannot be found."); + needCreateAddrs = true; + } else { + boolean needCreateIP4Addr = + actualIP4.getPrefixlen() != 32 || + !node.getIpv4().equals(actualIP4.getLocal()) || + !desired.getPeerIPv4().equals(actualIP4.getAddress()); + boolean needCreateIP6Addr = false; + if(desireIP6) { + needCreateIP6Addr = + actualIP6.getPrefixlen() != (linkLocal ? 64 : 128) || + !(linkLocal ? node.getIpv6() : node.getIpv6NonLL()).equals(actualIP6.getLocal()) || + (linkLocal ? (actualIP6.getAddress() != null) : + !desired.getPeerIPv6().equals(actualIP6.getAddress())); + if(needCreateIP6Addr) { + logger.info("IPv6 addresses for " + desired.getId() + " is outdated.\n" + + "Prefixes match: " + (actualIP6.getPrefixlen() == (linkLocal ? 64 : 128)) + "\n" + + "Local addresses match: " + ((linkLocal ? node.getIpv6() : node.getIpv6NonLL()).equals(actualIP6.getLocal())) + "\n" + + "Peer addresses match: " + (linkLocal ? (actualIP6.getAddress() == null) : + desired.getPeerIPv6().equals(actualIP6.getAddress()))); + } + } + needCreateAddrs = needCreateIP4Addr || needCreateIP6Addr; + if(needCreateAddrs) + logger.info("Recreating addresses for " + desired.getId() + + " since IPv4 or IPv6 information is updated: " + needCreateIP4Addr + ", " + needCreateIP6Addr + "."); + } + } - @Nonnull - private Future<Buffer> renderConfig(@Nonnull Node node, @Nonnull WireGuardConfig config) { - final Map<String, Object> params = new HashMap<>(9); - params.put("listen_port", config.getListenPort()); - params.put("self_priv_key", config.getSelfPrivKey()); - params.put("dev", config.getInterface()); - params.put("self_ipv4", node.getIpv4()); - params.put("peer_ipv4", config.getPeerIPv4()); - if (!config.getPeerIPv6().equals("")) { - params.put("peer_ipv6", config.getPeerIPv6()); - try { - final boolean ll = Inet6Address.getByName(config.getPeerIPv6()).isLinkLocalAddress(); - params.put("peer_ipv6_ll", ll); - if(ll) - params.put("self_ipv6", node.getIpv6()); + final List<List<String>> changes = new ArrayList<>(); + if(needCreateInterface) + changes.add(IP.Link.add(desired.getInterface(), "wireguard")); + if(needCreateAddrs) { + changes.add(IP.Addr.flush(desired.getInterface())); + changes.add(IP.Addr.add(node.getIpv4() + "/32", + desired.getInterface(), + desired.getPeerIPv4() + "/32")); + if(!desired.getPeerIPv6().isEmpty()) { + if(linkLocal) + changes.add(IP.Addr.add(node.getIpv6() + "/64", + desired.getInterface(), + null)); else - params.put("self_ipv6", node.getIpv6NonLL()); - } catch (IOException e) { - return Future.failedFuture(e); + changes.add(IP.Addr.add(node.getIpv6NonLL() + "/128", + desired.getInterface(), + desired.getPeerIPv6() + "/128")); } } - params.put("preshared_key", config.getSelfPresharedSecret()); - if(!config.getEndpoint().equals("")) { - params.put("endpoint", config.getEndpoint()); - } - params.put("peer_pub_key", config.getPeerPubKey()); - - return engine.render(params, "wg.conf.ftlh"); + if(needUp) + changes.add(IP.Link.set(desired.getInterface(), "up")); + return changes + .stream().map(cmd -> String.join(" ", cmd)) + .collect(Collectors.toList()); } @Nonnull - private Future<List<Change>> calculateSingleConfigChange(@Nonnull Node node, - @Nonnull WireGuardConfig desiredConfig) { - return CompositeFuture.all(readConfig(desiredConfig.getInterface()), renderConfig(node, desiredConfig)) - .compose(future -> { - final Buffer actualBuff = future.resultAt(0); - final String actual = actualBuff == null ? null : actualBuff.toString(); - final String desired = future.resultAt(1).toString(); - final List<Change> changes = new ArrayList<>(1); - if(actual == null) { - changes.add(new FileChange(generateWGPath(desiredConfig.getInterface()), - desired, - FileChange.Action.CREATE_AND_WRITE.toString())); - changes.add( - new CommandChange(new String[] { "systemctl", - "enable", - "--now", - "-q", - "wg-quick@" + desiredConfig.getInterface() })); - } else if(!actual.equals(desired)) { - // TODO: Smart reloading / restarting - changes.add(new FileChange(generateWGPath(desiredConfig.getInterface()), - desired, - FileChange.Action.OVERWRITE.toString())); - changes.add( - new CommandChange(new String[] { "systemctl", - "restart", - "-q", - "wg-quick@" + desiredConfig.getInterface() })); + private Future<List<Change>> calculateTotalNetlinkChanges(@Nonnull Node node, + @Nonnull List<WireGuardConfig> allDesired) { + return IP.ip(vertx, new IPOptions(), IP.Addr.show(null)) + .compose(IP.Addr::handler) + .compose(addrs -> { + final List<String> ipCommands = new ArrayList<>(); + for (final WireGuardConfig desired : allDesired) { + final Address actual = searchActualAddress(addrs, desired.getInterface()); + try { + ipCommands.addAll(calculateSingleNetlinkChanges(node, + desired, + actual)); + } catch (IOException e) { + return Future.failedFuture(e); + } + } + // Detect interfaces to delete + for (final Address address : addrs) { + if(!address.getLinkType().equals("none") || + !address.getIfname().matches("wg_.*")) { + continue; + } + if(searchDesiredConfig(allDesired, address.getIfname()) == null) + ipCommands.add(String.join(" ", IP.Link.del(address.getIfname()))); + } + final List<Change> changes = new ArrayList<>(); + if(!ipCommands.isEmpty()) { + changes.add(new IPChange(true, ipCommands)); } return Future.succeededFuture(changes); }); } @Nonnull - private Future<List<Change>> calculateSingleServiceStatusChange(@Nonnull WireGuardConfig desiredConfig) { - // Check if the service is not started or in wrong state. - return AsyncShell.exec(vertx, "systemctl", "is-active", "wg-quick@" + desiredConfig.getInterface()) - .compose(res -> { - if(res == 0) { - return Future.succeededFuture(Collections.emptyList()); + private Future<List<Change>> calculateTotalWireGuardChanges(@Nonnull Node node, + @Nonnull List<WireGuardConfig> allDesired) { + return CompositeFuture.join(allDesired.stream().map(desired -> { + return renderConfig(desired) + .compose(desiredConf -> { + return Future.succeededFuture(new WireGuardSyncConfChange(desired.getInterface(), + desiredConf.toString())); + }); + }).collect(Collectors.toList())) + .compose(compositeFuture -> { + final List<Change> changes = new ArrayList<>(allDesired.size()); + for (int i = 0; i < allDesired.size(); i ++) { + final Change change = compositeFuture.resultAt(i); + if(change == null) continue; + changes.add(change); } - return Future.succeededFuture(Collections.singletonList( - new CommandChange(new String[] { "systemctl", - "enable", - "--now", - "-q", - "wg-quick@" + desiredConfig.getInterface() }) - )); + return Future.succeededFuture(changes); }); } @Nonnull @Override public Future<List<Change>> calculateChanges(@Nonnull Node node, @Nonnull List<WireGuardConfig> allDesired) { - final List<Future<List<Change>>> addOrModifyChanges = - allDesired.stream().map(desired -> calculateSingleConfigChange(node, desired)).collect(Collectors.toList()); - final List<Future<List<Change>>> serviceChanges = - allDesired.stream().map(this::calculateSingleServiceStatusChange).collect(Collectors.toList()); - final Future<List<Change>> deleteChanges = - calculateDeleteChanges(allDesired); - final List<Future> futures = new ArrayList<>(addOrModifyChanges.size() + 1); - futures.addAll(addOrModifyChanges); - futures.addAll(serviceChanges); - futures.add(deleteChanges); - return CompositeFuture.all(futures) - .compose(compositeFuture -> { - final List<Change> changes = new ArrayList<>(futures.size()); - for(int i = 0; i < futures.size(); i ++) { - changes.addAll(compositeFuture.resultAt(i)); - } - return Future.succeededFuture(changes); - }); + return calculateDeleteChanges(allDesired).compose(changes -> { + return calculateTotalNetlinkChanges(node, allDesired) + .compose(netlinkChanges -> { + changes.addAll(netlinkChanges); + return Future.succeededFuture(changes); + }); + }).compose(changes -> { + return calculateTotalWireGuardChanges(node, allDesired) + .compose(wireguardChanges -> { + changes.addAll(wireguardChanges); + return Future.succeededFuture(changes); + }); + }); } } diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/WireGuardSyncConfChange.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/WireGuardSyncConfChange.java new file mode 100644 index 0000000..3c50694 --- /dev/null +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/WireGuardSyncConfChange.java @@ -0,0 +1,51 @@ +package moe.yuuta.dn42peering.agent.provision; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import moe.yuuta.dn42peering.agent.IOUtils; + +import javax.annotation.Nonnull; +import java.io.*; + +public class WireGuardSyncConfChange extends Change { + private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName()); + + private final String device; + private final String config; + public WireGuardSyncConfChange(@Nonnull String device, + @Nonnull String config) { + super(device, config, "syncconf"); + this.device = device; + this.config = config; + } + + @Nonnull + @Override + public Future<Void> execute(@Nonnull Vertx vertx) { + logger.info("Syncing config with " + device); + return vertx.executeBlocking(f -> { + try { + final Process process = new ProcessBuilder() + .command("wg", "syncconf", device, "/dev/stdin") + .start(); + final OutputStream stdin = process.getOutputStream(); + final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stdin)); + writer.write(config); + writer.close(); + final InputStream stderr = process.getErrorStream(); + final int res = process.waitFor(); + if(res != 0) { + f.fail("Unexpected 'wg syncconf " + device + " /dev/stdin' response: " + res + ".\n" + + "stderr = \n" + + IOUtils.read(stderr)); + return; + } + f.complete(); + } catch (IOException | InterruptedException e) { + f.fail(e); + } + }); + } +} diff --git a/agent/src/main/resources/wg.conf.ftlh b/agent/src/main/resources/wg.conf.ftlh index abd07cf..cc51ede 100644 --- a/agent/src/main/resources/wg.conf.ftlh +++ b/agent/src/main/resources/wg.conf.ftlh @@ -1,17 +1,6 @@ -# Automatically Generated by dn42peering Agent. - [Interface] ListenPort = ${listen_port?long?c} PrivateKey = ${self_priv_key} -PostUp = ip addr add dev ${dev} ${self_ipv4}/32 peer ${peer_ipv4}/32 -<#if peer_ipv6??> -<#if peer_ipv6_ll> -PostUp = ip addr add dev ${dev} ${self_ipv6}/64 -<#else> -PostUp = ip addr add dev ${dev} ${self_ipv6}/128 peer ${peer_ipv6}/128 -</#if> -</#if> -Table = off [Peer] PublicKey = ${peer_pub_key} diff --git a/rpc-common/src/main/java/moe/yuuta/dn42peering/RPC.java b/rpc-common/src/main/java/moe/yuuta/dn42peering/RPC.java index 3fe243b..18b3a0b 100644 --- a/rpc-common/src/main/java/moe/yuuta/dn42peering/RPC.java +++ b/rpc-common/src/main/java/moe/yuuta/dn42peering/RPC.java @@ -2,4 +2,9 @@ package moe.yuuta.dn42peering; public final class RPC { public static final int AGENT_PORT = 49200; + + // Agents before v1.11 + public static final int VERSION_LEGACY = -1; + // v1.11: Adds GetVersion() support. + public static final int VERSION_11 = 11; } |