Add DNS discovery (#1595)

* Add DNS discovery for goerli, rinkeby and mainnet to help speed up discovery of peers

Signed-off-by: Antoine Toulme <antoine@lunar-ocean.com>

* Add final to constant fields

Signed-off-by: Antoine Toulme <antoine@lunar-ocean.com>

Co-authored-by: David Mechler <david.mechler@consensys.net>
pull/1621/head
Antoine Toulme 4 years ago committed by GitHub
parent a4a8723b69
commit f41fb97ae4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java
  2. 53
      besu/src/main/java/org/hyperledger/besu/cli/config/EthNetworkConfig.java
  3. 5
      besu/src/test/java/org/hyperledger/besu/RunnerTest.java
  4. 7
      besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java
  5. 7
      ethereum/p2p/build.gradle
  6. 40
      ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/config/DiscoveryConfiguration.java
  7. 40
      ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/DefaultP2PNetwork.java
  8. 7
      gradle/versions.gradle

@ -350,6 +350,7 @@ public class RunnerBuilder {
bootstrap = ethNetworkConfig.getBootNodes();
}
discoveryConfiguration.setBootnodes(bootstrap);
discoveryConfiguration.setDnsDiscoveryURL(ethNetworkConfig.getDnsDiscoveryUrl());
} else {
discoveryConfiguration.setActive(false);
}

@ -17,10 +17,13 @@ package org.hyperledger.besu.cli.config;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hyperledger.besu.ethereum.p2p.config.DiscoveryConfiguration.CLASSIC_BOOTSTRAP_NODES;
import static org.hyperledger.besu.ethereum.p2p.config.DiscoveryConfiguration.GOERLI_BOOTSTRAP_NODES;
import static org.hyperledger.besu.ethereum.p2p.config.DiscoveryConfiguration.GOERLI_DISCOVERY_URL;
import static org.hyperledger.besu.ethereum.p2p.config.DiscoveryConfiguration.KOTTI_BOOTSTRAP_NODES;
import static org.hyperledger.besu.ethereum.p2p.config.DiscoveryConfiguration.MAINNET_BOOTSTRAP_NODES;
import static org.hyperledger.besu.ethereum.p2p.config.DiscoveryConfiguration.MAINNET_DISCOVERY_URL;
import static org.hyperledger.besu.ethereum.p2p.config.DiscoveryConfiguration.MORDOR_BOOTSTRAP_NODES;
import static org.hyperledger.besu.ethereum.p2p.config.DiscoveryConfiguration.RINKEBY_BOOTSTRAP_NODES;
import static org.hyperledger.besu.ethereum.p2p.config.DiscoveryConfiguration.RINKEBY_DISCOVERY_URL;
import static org.hyperledger.besu.ethereum.p2p.config.DiscoveryConfiguration.ROPSTEN_BOOTSTRAP_NODES;
import static org.hyperledger.besu.ethereum.p2p.config.DiscoveryConfiguration.YOLO_V2_BOOTSTRAP_NODES;
@ -59,14 +62,19 @@ public class EthNetworkConfig {
private final String genesisConfig;
private final BigInteger networkId;
private final List<EnodeURL> bootNodes;
private final String dnsDiscoveryUrl;
public EthNetworkConfig(
final String genesisConfig, final BigInteger networkId, final List<EnodeURL> bootNodes) {
final String genesisConfig,
final BigInteger networkId,
final List<EnodeURL> bootNodes,
final String dnsDiscoveryUrl) {
Preconditions.checkNotNull(genesisConfig);
Preconditions.checkNotNull(bootNodes);
this.genesisConfig = genesisConfig;
this.networkId = networkId;
this.bootNodes = bootNodes;
this.dnsDiscoveryUrl = dnsDiscoveryUrl;
}
public String getGenesisConfig() {
@ -81,6 +89,10 @@ public class EthNetworkConfig {
return bootNodes;
}
public String getDnsDiscoveryUrl() {
return dnsDiscoveryUrl;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
@ -92,12 +104,13 @@ public class EthNetworkConfig {
final EthNetworkConfig that = (EthNetworkConfig) o;
return networkId.equals(that.networkId)
&& Objects.equals(genesisConfig, that.genesisConfig)
&& Objects.equals(bootNodes, that.bootNodes);
&& Objects.equals(bootNodes, that.bootNodes)
&& Objects.equals(dnsDiscoveryUrl, that.dnsDiscoveryUrl);
}
@Override
public int hashCode() {
return Objects.hash(genesisConfig, networkId, bootNodes);
return Objects.hash(genesisConfig, networkId, bootNodes, dnsDiscoveryUrl);
}
@Override
@ -109,6 +122,8 @@ public class EthNetworkConfig {
+ networkId
+ ", bootNodes="
+ bootNodes
+ ", dnsDiscoveryUrl="
+ dnsDiscoveryUrl
+ '}';
}
@ -116,31 +131,41 @@ public class EthNetworkConfig {
switch (networkName) {
case ROPSTEN:
return new EthNetworkConfig(
jsonConfig(ROPSTEN_GENESIS), ROPSTEN_NETWORK_ID, ROPSTEN_BOOTSTRAP_NODES);
jsonConfig(ROPSTEN_GENESIS), ROPSTEN_NETWORK_ID, ROPSTEN_BOOTSTRAP_NODES, null);
case RINKEBY:
return new EthNetworkConfig(
jsonConfig(RINKEBY_GENESIS), RINKEBY_NETWORK_ID, RINKEBY_BOOTSTRAP_NODES);
jsonConfig(RINKEBY_GENESIS),
RINKEBY_NETWORK_ID,
RINKEBY_BOOTSTRAP_NODES,
RINKEBY_DISCOVERY_URL);
case GOERLI:
return new EthNetworkConfig(
jsonConfig(GOERLI_GENESIS), GOERLI_NETWORK_ID, GOERLI_BOOTSTRAP_NODES);
jsonConfig(GOERLI_GENESIS),
GOERLI_NETWORK_ID,
GOERLI_BOOTSTRAP_NODES,
GOERLI_DISCOVERY_URL);
case DEV:
return new EthNetworkConfig(jsonConfig(DEV_GENESIS), DEV_NETWORK_ID, new ArrayList<>());
return new EthNetworkConfig(
jsonConfig(DEV_GENESIS), DEV_NETWORK_ID, new ArrayList<>(), null);
case CLASSIC:
return new EthNetworkConfig(
jsonConfig(CLASSIC_GENESIS), CLASSIC_NETWORK_ID, CLASSIC_BOOTSTRAP_NODES);
jsonConfig(CLASSIC_GENESIS), CLASSIC_NETWORK_ID, CLASSIC_BOOTSTRAP_NODES, null);
case KOTTI:
return new EthNetworkConfig(
jsonConfig(KOTTI_GENESIS), KOTTI_NETWORK_ID, KOTTI_BOOTSTRAP_NODES);
jsonConfig(KOTTI_GENESIS), KOTTI_NETWORK_ID, KOTTI_BOOTSTRAP_NODES, null);
case MORDOR:
return new EthNetworkConfig(
jsonConfig(MORDOR_GENESIS), MORDOR_NETWORK_ID, MORDOR_BOOTSTRAP_NODES);
jsonConfig(MORDOR_GENESIS), MORDOR_NETWORK_ID, MORDOR_BOOTSTRAP_NODES, null);
case YOLO_V2:
return new EthNetworkConfig(
jsonConfig(YOLO_GENESIS), YOLO_V2_NETWORK_ID, YOLO_V2_BOOTSTRAP_NODES);
jsonConfig(YOLO_GENESIS), YOLO_V2_NETWORK_ID, YOLO_V2_BOOTSTRAP_NODES, null);
case MAINNET:
default:
return new EthNetworkConfig(
jsonConfig(MAINNET_GENESIS), MAINNET_NETWORK_ID, MAINNET_BOOTSTRAP_NODES);
jsonConfig(MAINNET_GENESIS),
MAINNET_NETWORK_ID,
MAINNET_BOOTSTRAP_NODES,
MAINNET_DISCOVERY_URL);
}
}
@ -180,6 +205,7 @@ public class EthNetworkConfig {
public static class Builder {
private final String dnsDiscoveryUrl;
private String genesisConfig;
private BigInteger networkId;
private List<EnodeURL> bootNodes;
@ -188,6 +214,7 @@ public class EthNetworkConfig {
this.genesisConfig = ethNetworkConfig.genesisConfig;
this.networkId = ethNetworkConfig.networkId;
this.bootNodes = ethNetworkConfig.bootNodes;
this.dnsDiscoveryUrl = ethNetworkConfig.dnsDiscoveryUrl;
}
public Builder setGenesisConfig(final String genesisConfig) {
@ -206,7 +233,7 @@ public class EthNetworkConfig {
}
public EthNetworkConfig build() {
return new EthNetworkConfig(genesisConfig, networkId, bootNodes);
return new EthNetworkConfig(genesisConfig, networkId, bootNodes, dnsDiscoveryUrl);
}
}
}

@ -254,7 +254,10 @@ public final class RunnerTest {
final EnodeURL enode = runnerAhead.getLocalEnode().get();
final EthNetworkConfig behindEthNetworkConfiguration =
new EthNetworkConfig(
EthNetworkConfig.jsonConfig(DEV), DEV_NETWORK_ID, Collections.singletonList(enode));
EthNetworkConfig.jsonConfig(DEV),
DEV_NETWORK_ID,
Collections.singletonList(enode),
null);
runnerBehind =
runnerBuilder
.besuController(controllerBehind)

@ -31,6 +31,7 @@ import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.NET;
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.PERM;
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.WEB3;
import static org.hyperledger.besu.ethereum.p2p.config.DiscoveryConfiguration.MAINNET_BOOTSTRAP_NODES;
import static org.hyperledger.besu.ethereum.p2p.config.DiscoveryConfiguration.MAINNET_DISCOVERY_URL;
import static org.hyperledger.besu.nat.kubernetes.KubernetesNatManager.DEFAULT_BESU_SERVICE_NAME_FILTER;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
@ -173,7 +174,8 @@ public class BesuCommandTest extends CommandTestAbstract {
new EthNetworkConfig(
EthNetworkConfig.jsonConfig(MAINNET),
EthNetworkConfig.MAINNET_NETWORK_ID,
MAINNET_BOOTSTRAP_NODES));
MAINNET_BOOTSTRAP_NODES,
MAINNET_DISCOVERY_URL));
verify(mockRunnerBuilder).p2pAdvertisedHost(eq("127.0.0.1"));
verify(mockRunnerBuilder).p2pListenPort(eq(30303));
verify(mockRunnerBuilder).maxPeers(eq(25));
@ -784,7 +786,8 @@ public class BesuCommandTest extends CommandTestAbstract {
new EthNetworkConfig(
EthNetworkConfig.jsonConfig(MAINNET),
EthNetworkConfig.MAINNET_NETWORK_ID,
MAINNET_BOOTSTRAP_NODES));
MAINNET_BOOTSTRAP_NODES,
MAINNET_DISCOVERY_URL));
verify(mockRunnerBuilder).p2pAdvertisedHost(eq("127.0.0.1"));
verify(mockRunnerBuilder).p2pListenPort(eq(30303));
verify(mockRunnerBuilder).maxPeers(eq(25));

@ -35,11 +35,18 @@ dependencies {
implementation project(':nat')
implementation 'com.google.guava:guava'
implementation 'dnsjava:dnsjava'
implementation 'io.prometheus:simpleclient'
implementation 'io.vertx:vertx-core'
implementation 'org.apache.logging.log4j:log4j-api'
implementation 'org.apache.tuweni:bytes'
implementation 'org.apache.tuweni:crypto'
implementation 'org.apache.tuweni:devp2p'
implementation 'org.apache.tuweni:dns-discovery'
implementation 'org.apache.tuweni:io'
implementation 'org.apache.tuweni:rlp'
implementation 'org.apache.tuweni:units'
implementation 'org.jetbrains.kotlin:kotlin-stdlib'
implementation 'org.xerial.snappy:snappy-java'
annotationProcessor "org.immutables:value"

@ -27,7 +27,14 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
public class DiscoveryConfiguration {
public static List<EnodeURL> MAINNET_BOOTSTRAP_NODES =
public static final String GOERLI_DISCOVERY_URL =
"enrtree://AKA3AM6LPBYEUDMVNU3BSVQJ5AD45Y7YPOHJLEF6W26QOE4VTUDPE@all.goerli.ethdisco.net";
public static final String MAINNET_DISCOVERY_URL =
"enrtree://AKA3AM6LPBYEUDMVNU3BSVQJ5AD45Y7YPOHJLEF6W26QOE4VTUDPE@all.mainnet.ethdisco.net";
public static final String RINKEBY_DISCOVERY_URL =
"enrtree://AKA3AM6LPBYEUDMVNU3BSVQJ5AD45Y7YPOHJLEF6W26QOE4VTUDPE@all.rinkeby.ethdisco.net";
public static final List<EnodeURL> MAINNET_BOOTSTRAP_NODES =
Collections.unmodifiableList(
Stream.of(
// Ethereum Foundation Bootnodes
@ -52,7 +59,7 @@ public class DiscoveryConfiguration {
)
.map(EnodeURL::fromString)
.collect(toList()));
public static List<EnodeURL> RINKEBY_BOOTSTRAP_NODES =
public static final List<EnodeURL> RINKEBY_BOOTSTRAP_NODES =
Collections.unmodifiableList(
Stream.of(
"enode://a24ac7c5484ef4ed0c5eb2d36620ba4e4aa13b8c84684e1b4aab0cebea2ae45cb4d375b77eab56516d34bfbd3c1a833fc51296ff084b770b94fb9028c4d25ccf@52.169.42.101:30303",
@ -60,7 +67,7 @@ public class DiscoveryConfiguration {
"enode://b6b28890b006743680c52e64e0d16db57f28124885595fa03a562be1d2bf0f3a1da297d56b13da25fb992888fd556d4c1a27b1f39d531bde7de1921c90061cc6@159.89.28.211:30303")
.map(EnodeURL::fromString)
.collect(toList()));
public static List<EnodeURL> ROPSTEN_BOOTSTRAP_NODES =
public static final List<EnodeURL> ROPSTEN_BOOTSTRAP_NODES =
Collections.unmodifiableList(
Stream.of(
"enode://6332792c4a00e3e4ee0926ed89e0d27ef985424d97b6a45bf0f23e51f0dcb5e66b875777506458aea7af6f9e4ffb69f43f3778ee73c81ed9d34c51c4b16b0b0f@52.232.243.152:30303",
@ -70,7 +77,7 @@ public class DiscoveryConfiguration {
.map(EnodeURL::fromString)
.collect(toList()));
public static List<EnodeURL> GOERLI_BOOTSTRAP_NODES =
public static final List<EnodeURL> GOERLI_BOOTSTRAP_NODES =
Collections.unmodifiableList(
Stream.of(
"enode://011f758e6552d105183b1761c5e2dea0111bc20fd5f6422bc7f91e0fabbec9a6595caf6239b37feb773dddd3f87240d99d859431891e4a642cf2a0a9e6cbb98a@51.141.78.53:30303",
@ -89,7 +96,7 @@ public class DiscoveryConfiguration {
.map(EnodeURL::fromString)
.collect(toList()));
public static List<EnodeURL> CLASSIC_BOOTSTRAP_NODES =
public static final List<EnodeURL> CLASSIC_BOOTSTRAP_NODES =
Collections.unmodifiableList(
Stream.of(
"enode://158ac5a4817265d0d8b977660b3dbe9abee5694ed212f7091cbf784ddf47623ed015e1cb54594d10c1c46118747ddabe86ebf569cf24ae91f2daa0f1adaae390@159.203.56.33:30303",
@ -114,7 +121,7 @@ public class DiscoveryConfiguration {
.map(EnodeURL::fromString)
.collect(toList()));
public static List<EnodeURL> KOTTI_BOOTSTRAP_NODES =
public static final List<EnodeURL> KOTTI_BOOTSTRAP_NODES =
Collections.unmodifiableList(
Stream.of(
// Authority Nodes
@ -134,7 +141,7 @@ public class DiscoveryConfiguration {
.map(EnodeURL::fromString)
.collect(toList()));
public static List<EnodeURL> MORDOR_BOOTSTRAP_NODES =
public static final List<EnodeURL> MORDOR_BOOTSTRAP_NODES =
Collections.unmodifiableList(
Stream.of(
"enode://642cf9650dd8869d42525dbf6858012e3b4d64f475e733847ab6f7742341a4397414865d953874e8f5ed91b0e4e1c533dee14ad1d6bb276a5459b2471460ff0d@157.230.152.87:30303", // @meowbits Mordor
@ -169,7 +176,7 @@ public class DiscoveryConfiguration {
.map(EnodeURL::fromString)
.collect(toList()));
public static List<EnodeURL> YOLO_V2_BOOTSTRAP_NODES =
public static final List<EnodeURL> YOLO_V2_BOOTSTRAP_NODES =
Collections.unmodifiableList(
Stream.<String>of(
"enode://9e1096aa59862a6f164994cb5cb16f5124d6c992cdbf4535ff7dea43ea1512afe5448dca9df1b7ab0726129603f1a3336b631e4d7a1a44c94daddd03241587f9@3.9.20.133:30303")
@ -182,6 +189,7 @@ public class DiscoveryConfiguration {
private String advertisedHost = "127.0.0.1";
private int bucketSize = 16;
private List<EnodeURL> bootnodes = new ArrayList<>();
private String dnsDiscoveryURL;
public static DiscoveryConfiguration create() {
return new DiscoveryConfiguration();
@ -255,6 +263,14 @@ public class DiscoveryConfiguration {
return this;
}
public String getDNSDiscoveryURL() {
return dnsDiscoveryURL;
}
public void setDnsDiscoveryURL(final String dnsDiscoveryURL) {
this.dnsDiscoveryURL = dnsDiscoveryURL;
}
@Override
public boolean equals(final Object o) {
if (o == this) {
@ -269,12 +285,14 @@ public class DiscoveryConfiguration {
&& bucketSize == that.bucketSize
&& Objects.equals(bindHost, that.bindHost)
&& Objects.equals(advertisedHost, that.advertisedHost)
&& Objects.equals(bootnodes, that.bootnodes);
&& Objects.equals(bootnodes, that.bootnodes)
&& Objects.equals(dnsDiscoveryURL, that.dnsDiscoveryURL);
}
@Override
public int hashCode() {
return Objects.hash(active, bindHost, bindPort, advertisedHost, bucketSize, bootnodes);
return Objects.hash(
active, bindHost, bindPort, advertisedHost, bucketSize, bootnodes, dnsDiscoveryURL);
}
@Override
@ -294,6 +312,8 @@ public class DiscoveryConfiguration {
+ bucketSize
+ ", bootnodes="
+ bootnodes
+ ", dnsDiscoveryURL="
+ dnsDiscoveryURL
+ '}';
}
}

@ -49,6 +49,7 @@ import org.hyperledger.besu.nat.upnp.UpnpNatManager;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
@ -61,6 +62,7 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -69,6 +71,8 @@ import io.vertx.core.Vertx;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.devp2p.EthereumNodeRecord;
import org.apache.tuweni.discovery.DNSDaemon;
/**
* The peer network service (defunct PeerNetworkingService) is the entrypoint to the peer-to-peer
@ -136,6 +140,9 @@ public class DefaultP2PNetwork implements P2PNetwork {
private final CountDownLatch shutdownLatch = new CountDownLatch(2);
private final Duration shutdownTimeout = Duration.ofMinutes(1);
private final AtomicReference<List<DiscoveryPeer>> dnsPeers = new AtomicReference<>();
private DNSDaemon dnsDaemon;
/**
* Creates a peer networking service for production purposes.
*
@ -163,7 +170,6 @@ public class DefaultP2PNetwork implements P2PNetwork {
final NatService natService,
final MaintainedPeers maintainedPeers,
final PeerReputationManager reputationManager) {
this.localNode = localNode;
this.peerDiscoveryAgent = peerDiscoveryAgent;
this.rlpxAgent = rlpxAgent;
@ -193,6 +199,30 @@ public class DefaultP2PNetwork implements P2PNetwork {
final String address = config.getDiscovery().getAdvertisedHost();
final int configuredDiscoveryPort = config.getDiscovery().getBindPort();
final int configuredRlpxPort = config.getRlpx().getBindPort();
if (config.getDiscovery().getDNSDiscoveryURL() != null) {
LOG.info("Starting DNS discovery with URL {}", config.getDiscovery().getDNSDiscoveryURL());
dnsDaemon = new DNSDaemon(config.getDiscovery().getDNSDiscoveryURL());
dnsDaemon
.getListeners()
.add(
(seq, records) -> {
List<DiscoveryPeer> peers = new ArrayList<>();
for (EthereumNodeRecord enr : records) {
EnodeURL enodeURL =
EnodeURL.builder()
.ipAddress(enr.ip())
.nodeId(enr.publicKey().bytes())
.discoveryPort(Optional.ofNullable(enr.udp()))
.listeningPort(Optional.ofNullable(enr.tcp()))
.build();
DiscoveryPeer peer = DiscoveryPeer.fromEnode(enodeURL);
peers.add(peer);
rlpxAgent.connect(peer);
}
dnsPeers.set(peers);
return null;
});
}
final int listeningPort = rlpxAgent.start().join();
final int discoveryPort =
@ -235,6 +265,10 @@ public class DefaultP2PNetwork implements P2PNetwork {
return;
}
if (dnsDaemon != null) {
dnsDaemon.close();
}
peerConnectionScheduler.shutdownNow();
peerDiscoveryAgent.stop().whenComplete((res, err) -> shutdownLatch.countDown());
rlpxAgent.stop().whenComplete((res, err) -> shutdownLatch.countDown());
@ -306,6 +340,10 @@ public class DefaultP2PNetwork implements P2PNetwork {
@Override
public Stream<DiscoveryPeer> streamDiscoveredPeers() {
List<DiscoveryPeer> peers = dnsPeers.get();
if (peers != null) {
return Stream.concat(peerDiscoveryAgent.streamDiscoveredPeers(), peers.stream());
}
return peerDiscoveryAgent.streamDiscoveredPeers();
}

@ -43,6 +43,8 @@ dependencyManagement {
dependency 'commons-io:commons-io:2.7'
dependency 'dnsjava:dnsjava:3.0.2'
dependency 'info.picocli:picocli:4.5.0'
dependency 'io.grpc:grpc-netty:1.33.0'
@ -89,7 +91,10 @@ dependencyManagement {
dependency 'org.apache.tuweni:bytes:1.2.0'
dependency 'org.apache.tuweni:config:1.2.0'
dependency 'org.apache.tuweni:crypto:1.2.0'
dependency 'org.apache.tuweni:devp2p:1.2.0'
dependency 'org.apache.tuweni:dns-discovery:1.2.0'
dependency 'org.apache.tuweni:io:1.2.0'
dependency 'org.apache.tuweni:rlp:1.2.0'
dependency 'org.apache.tuweni:toml:1.2.0'
dependency 'org.apache.tuweni:units:1.2.0'
dependency 'org.apache.tuweni:net:1.2.0'
@ -106,6 +111,8 @@ dependencyManagement {
dependency 'org.java-websocket:Java-WebSocket:1.5.1'
dependency 'org.jetbrains.kotlin:kotlin-stdlib:1.3.20'
dependency 'org.jupnp:org.jupnp.support:2.5.2'
dependency 'org.jupnp:org.jupnp:2.5.2'

Loading…
Cancel
Save