Hole Punching (#806)

Co-authored-by: Tanguy <tanguy@status.im>
This commit is contained in:
diegomrsantos 2023-04-18 12:50:21 +02:00 committed by GitHub
parent b7726bf68f
commit a5666789b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 365 additions and 62 deletions

View File

@ -20,8 +20,6 @@ import ../../../switch,
../../../peerid
import core
export core
logScope:
topics = "libp2p autonat"

View File

@ -17,9 +17,12 @@ import chronos, metrics
import ../../../switch
import ../../../wire
import client
from core import NetworkReachability, AutonatUnreachableError
import ../../../utils/heartbeat
import ../../../crypto/crypto
export options, core.NetworkReachability
logScope:
topics = "libp2p autonatservice"
@ -30,7 +33,7 @@ type
newConnectedPeerHandler: PeerEventHandler
addressMapper: AddressMapper
scheduleHandle: Future[void]
networkReachability: NetworkReachability
networkReachability*: NetworkReachability
confidence: Option[float]
answers: Deque[NetworkReachability]
autonatClient: AutonatClient
@ -71,9 +74,6 @@ proc new*(
dialTimeout: dialTimeout,
enableAddressMapper: enableAddressMapper)
proc networkReachability*(self: AutonatService): NetworkReachability {.inline.} =
return self.networkReachability
proc callHandler(self: AutonatService) {.async.} =
if not isNil(self.statusAndConfidenceHandler):
await self.statusAndConfidenceHandler(self.networkReachability, self.confidence)

View File

@ -23,6 +23,8 @@ import ../../protocol,
../../../switch,
../../../utils/future
export DcutrError
type
DcutrClient* = ref object
connectTimeout: Duration

View File

@ -23,6 +23,7 @@ import ../../protocol,
../../../switch,
../../../utils/future
export DcutrError
export chronicles
type Dcutr* = ref object of LPProtocol

View File

@ -101,7 +101,7 @@ proc createReserveResponse(
status: some(Ok))
return ok(msg)
proc isRelayed(conn: Connection): bool =
proc isRelayed*(conn: Connection): bool =
var wrappedConn = conn
while not isNil(wrappedConn):
if wrappedConn of RelayConnection:

View File

@ -0,0 +1,105 @@
# Nim-LibP2P
# Copyright (c) 2022 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.
when (NimMajor, NimMinor) < (1, 4):
{.push raises: [Defect].}
else:
{.push raises: [].}
import std/[tables, sequtils]
import chronos, chronicles
import ../switch, ../wire
import ../protocols/rendezvous
import ../services/autorelayservice
import ../discovery/[rendezvousinterface, discoverymngr]
import ../protocols/connectivity/relay/relay
import ../protocols/connectivity/autonat/service
import ../protocols/connectivity/dcutr/[client, server]
logScope:
topics = "libp2p hpservice"
type
HPService* = ref object of Service
newConnectedPeerHandler: PeerEventHandler
onNewStatusHandler: StatusAndConfidenceHandler
autoRelayService: AutoRelayService
autonatService: AutonatService
isPublicIPAddrProc: IsPublicIPAddrProc
IsPublicIPAddrProc* = proc(ta: TransportAddress): bool {.gcsafe, raises: [Defect].}
proc new*(T: typedesc[HPService], autonatService: AutonatService, autoRelayService: AutoRelayService,
isPublicIPAddrProc: IsPublicIPAddrProc = isGlobal): T =
return T(autonatService: autonatService, autoRelayService: autoRelayService, isPublicIPAddrProc: isPublicIPAddrProc)
proc tryStartingDirectConn(self: HPService, switch: Switch, peerId: PeerId): Future[bool] {.async.} =
await sleepAsync(500.milliseconds) # wait for AddressBook to be populated
for address in switch.peerStore[AddressBook][peerId]:
try:
let ta = initTAddress(address)
if ta.isOk() and self.isPublicIPAddrProc(ta.get()):
await switch.connect(peerId, @[address], true, false)
debug "Direct connection created."
return true
except CatchableError as err:
debug "Failed to create direct connection.", err = err.msg
continue
return false
method setup*(self: HPService, switch: Switch): Future[bool] {.async.} =
var hasBeenSetup = await procCall Service(self).setup(switch)
hasBeenSetup = hasBeenSetup and await self.autonatService.setup(switch)
if hasBeenSetup:
let dcutrProto = Dcutr.new(switch)
switch.mount(dcutrProto)
self.newConnectedPeerHandler = proc (peerId: PeerId, event: PeerEvent): Future[void] {.async.} =
try:
let conn = switch.connManager.selectMuxer(peerId).connection
if isRelayed(conn) and conn.transportDir == Direction.In:
if await self.tryStartingDirectConn(switch, peerId):
await conn.close()
return
let dcutrClient = DcutrClient.new()
var natAddrs = switch.peerStore.getMostObservedProtosAndPorts()
if natAddrs.len == 0:
natAddrs = switch.peerInfo.listenAddrs.mapIt(switch.peerStore.guessDialableAddr(it))
await dcutrClient.startSync(switch, peerId, natAddrs)
await sleepAsync(2000.milliseconds) # grace period before closing relayed connection
await conn.close()
except CatchableError as err:
debug "Hole punching failed during dcutr", err = err.msg
switch.connManager.addPeerEventHandler(self.newConnectedPeerHandler, PeerEventKind.Joined)
self.onNewStatusHandler = proc (networkReachability: NetworkReachability, confidence: Option[float]) {.gcsafe, async.} =
if networkReachability == NetworkReachability.NotReachable:
discard await self.autoRelayService.setup(switch)
elif networkReachability == NetworkReachability.Reachable:
discard await self.autoRelayService.stop(switch)
# We do it here instead of in the AutonatService because this is useful only when hole punching.
for t in switch.transports:
t.networkReachability = networkReachability
self.autonatService.statusAndConfidenceHandler(self.onNewStatusHandler)
return hasBeenSetup
method run*(self: HPService, switch: Switch) {.async, public.} =
await self.autonatService.run(switch)
method stop*(self: HPService, switch: Switch): Future[bool] {.async, public.} =
discard await self.autonatService.stop(switch)
if not isNil(self.newConnectedPeerHandler):
switch.connManager.removePeerEventHandler(self.newConnectedPeerHandler, PeerEventKind.Joined)

View File

@ -71,7 +71,7 @@ proc initTAddress*(ma: MultiAddress): MaResult[TransportAddress] =
res.port = Port(fromBytesBE(uint16, pbuf))
ok(res)
else:
err("MultiAddress must be wire address (tcp, udp or unix)")
err("MultiAddress must be wire address (tcp, udp or unix): " & $ma)
proc connect*(
ma: MultiAddress,

View File

@ -19,6 +19,7 @@ import ../../libp2p/[protocols/connectivity/autonat/client,
peerid,
multiaddress,
switch]
from ../../libp2p/protocols/connectivity/autonat/core import NetworkReachability, AutonatUnreachableError, AutonatError
type
AutonatClientStub* = ref object of AutonatClient

View File

@ -0,0 +1,48 @@
# Nim-LibP2P
# Copyright (c) 2023 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.
{.used.}
when (NimMajor, NimMinor) < (1, 4):
{.push raises: [Defect].}
else:
{.push raises: [].}
import chronos
import ../../libp2p/[peerid, multiaddress, switch]
type
SwitchStub* = ref object of Switch
switch*: Switch
connectStub*: proc(): Future[void] {.async.}
method connect*(
self: SwitchStub,
peerId: PeerId,
addrs: seq[MultiAddress],
forceDial = false,
reuseConnection = true,
upgradeDir = Direction.Out) {.async.} =
if (self.connectStub != nil):
await self.connectStub()
else:
await self.switch.connect(peerId, addrs, forceDial, reuseConnection, upgradeDir)
proc new*(T: typedesc[SwitchStub], switch: Switch, connectStub: proc (): Future[void] {.async.} = nil): T =
return SwitchStub(
switch: switch,
peerInfo: switch.peerInfo,
ms: switch.ms,
transports: switch.transports,
connManager: switch.connManager,
peerStore: switch.peerStore,
dialer: switch.dialer,
nameResolver: switch.nameResolver,
services: switch.services,
connectStub: connectStub)

View File

@ -1,3 +1,12 @@
# Nim-LibP2P
# Copyright (c) 2023 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.
{.used.}
when (NimMajor, NimMinor) < (1, 4):

View File

@ -51,7 +51,7 @@ suite "Autonat Service":
let switch3 = createSwitch()
let switch4 = createSwitch()
check autonatService.networkReachability() == NetworkReachability.Unknown
check autonatService.networkReachability == NetworkReachability.Unknown
await switch1.start()
await switch2.start()
@ -64,7 +64,7 @@ suite "Autonat Service":
await autonatClientStub.finished
check autonatService.networkReachability() == NetworkReachability.NotReachable
check autonatService.networkReachability == NetworkReachability.NotReachable
check libp2p_autonat_reachability_confidence.value(["NotReachable"]) == 0.3
await allFuturesThrowing(
@ -86,7 +86,7 @@ suite "Autonat Service":
if not awaiter.finished:
awaiter.complete()
check autonatService.networkReachability() == NetworkReachability.Unknown
check autonatService.networkReachability == NetworkReachability.Unknown
autonatService.statusAndConfidenceHandler(statusAndConfidenceHandler)
@ -101,7 +101,7 @@ suite "Autonat Service":
await awaiter
check autonatService.networkReachability() == NetworkReachability.Reachable
check autonatService.networkReachability == NetworkReachability.Reachable
check libp2p_autonat_reachability_confidence.value(["Reachable"]) == 0.3
check switch1.peerInfo.addrs == switch1.peerInfo.listenAddrs.mapIt(switch1.peerStore.guessDialableAddr(it))
@ -131,7 +131,7 @@ suite "Autonat Service":
autonatClientStub.answer = Reachable
awaiter.complete()
check autonatService.networkReachability() == NetworkReachability.Unknown
check autonatService.networkReachability == NetworkReachability.Unknown
autonatService.statusAndConfidenceHandler(statusAndConfidenceHandler)
@ -146,12 +146,12 @@ suite "Autonat Service":
await awaiter
check autonatService.networkReachability() == NetworkReachability.NotReachable
check autonatService.networkReachability == NetworkReachability.NotReachable
check libp2p_autonat_reachability_confidence.value(["NotReachable"]) == 0.3
await autonatClientStub.finished
check autonatService.networkReachability() == NetworkReachability.Reachable
check autonatService.networkReachability == NetworkReachability.Reachable
check libp2p_autonat_reachability_confidence.value(["Reachable"]) == 0.3
await allFuturesThrowing(switch1.stop(), switch2.stop(), switch3.stop(), switch4.stop())
@ -172,7 +172,7 @@ suite "Autonat Service":
if not awaiter.finished:
awaiter.complete()
check autonatService.networkReachability() == NetworkReachability.Unknown
check autonatService.networkReachability == NetworkReachability.Unknown
autonatService.statusAndConfidenceHandler(statusAndConfidenceHandler)
@ -187,7 +187,7 @@ suite "Autonat Service":
await awaiter
check autonatService.networkReachability() == NetworkReachability.Reachable
check autonatService.networkReachability == NetworkReachability.Reachable
check libp2p_autonat_reachability_confidence.value(["Reachable"]) == 1
await allFuturesThrowing(
@ -213,7 +213,7 @@ suite "Autonat Service":
autonatClientStub.answer = Unknown
awaiter.complete()
check autonatService.networkReachability() == NetworkReachability.Unknown
check autonatService.networkReachability == NetworkReachability.Unknown
autonatService.statusAndConfidenceHandler(statusAndConfidenceHandler)
@ -228,12 +228,12 @@ suite "Autonat Service":
await awaiter
check autonatService.networkReachability() == NetworkReachability.NotReachable
check autonatService.networkReachability == NetworkReachability.NotReachable
check libp2p_autonat_reachability_confidence.value(["NotReachable"]) == 1/3
await autonatClientStub.finished
check autonatService.networkReachability() == NetworkReachability.NotReachable
check autonatService.networkReachability == NetworkReachability.NotReachable
check libp2p_autonat_reachability_confidence.value(["NotReachable"]) == 1/3
await allFuturesThrowing(switch1.stop(), switch2.stop(), switch3.stop(), switch4.stop())
@ -264,7 +264,7 @@ suite "Autonat Service":
if not awaiter.finished:
awaiter.complete()
check autonatService.networkReachability() == NetworkReachability.Unknown
check autonatService.networkReachability == NetworkReachability.Unknown
autonatService.statusAndConfidenceHandler(statusAndConfidenceHandler)
@ -275,7 +275,7 @@ suite "Autonat Service":
await awaiter
check autonatService.networkReachability() == NetworkReachability.Reachable
check autonatService.networkReachability == NetworkReachability.Reachable
check libp2p_autonat_reachability_confidence.value(["Reachable"]) == 1
await allFuturesThrowing(
@ -304,8 +304,8 @@ suite "Autonat Service":
if not awaiter2.finished:
awaiter2.complete()
check autonatService1.networkReachability() == NetworkReachability.Unknown
check autonatService2.networkReachability() == NetworkReachability.Unknown
check autonatService1.networkReachability == NetworkReachability.Unknown
check autonatService2.networkReachability == NetworkReachability.Unknown
autonatService1.statusAndConfidenceHandler(statusAndConfidenceHandler1)
autonatService2.statusAndConfidenceHandler(statusAndConfidenceHandler2)
@ -321,8 +321,8 @@ suite "Autonat Service":
await awaiter1
await awaiter2
check autonatService1.networkReachability() == NetworkReachability.Reachable
check autonatService2.networkReachability() == NetworkReachability.Reachable
check autonatService1.networkReachability == NetworkReachability.Reachable
check autonatService2.networkReachability == NetworkReachability.Reachable
check libp2p_autonat_reachability_confidence.value(["Reachable"]) == 1
await allFuturesThrowing(
@ -342,7 +342,7 @@ suite "Autonat Service":
if not awaiter1.finished:
awaiter1.complete()
check autonatService1.networkReachability() == NetworkReachability.Unknown
check autonatService1.networkReachability == NetworkReachability.Unknown
autonatService1.statusAndConfidenceHandler(statusAndConfidenceHandler1)
@ -360,7 +360,7 @@ suite "Autonat Service":
await awaiter1
check autonatService1.networkReachability() == NetworkReachability.Reachable
check autonatService1.networkReachability == NetworkReachability.Reachable
check libp2p_autonat_reachability_confidence.value(["Reachable"]) == 1
# Make sure remote peer can't create a connection to us
@ -385,7 +385,7 @@ suite "Autonat Service":
if not awaiter.finished:
awaiter.complete()
check autonatService.networkReachability() == NetworkReachability.Unknown
check autonatService.networkReachability == NetworkReachability.Unknown
autonatService.statusAndConfidenceHandler(statusAndConfidenceHandler)
@ -407,7 +407,7 @@ suite "Autonat Service":
await autonatService.run(switch1)
await awaiter
check autonatService.networkReachability() == NetworkReachability.Reachable
check autonatService.networkReachability == NetworkReachability.Reachable
check libp2p_autonat_reachability_confidence.value(["Reachable"]) == 1
await allFuturesThrowing(
@ -422,7 +422,7 @@ suite "Autonat Service":
proc statusAndConfidenceHandler(networkReachability: NetworkReachability, confidence: Option[float]) {.gcsafe, async.} =
fail()
check autonatService.networkReachability() == NetworkReachability.Unknown
check autonatService.networkReachability == NetworkReachability.Unknown
autonatService.statusAndConfidenceHandler(statusAndConfidenceHandler)

View File

@ -16,36 +16,7 @@ from ../libp2p/protocols/connectivity/autonat/core import NetworkReachability
import ../libp2p/builders
import ../libp2p/utils/future
import ./helpers
type
SwitchStub* = ref object of Switch
switch: Switch
connectStub*: proc(): Future[void] {.async.}
proc new*(T: typedesc[SwitchStub], switch: Switch, connectStub: proc (): Future[void] {.async.} = nil): T =
return SwitchStub(
switch: switch,
peerInfo: switch.peerInfo,
ms: switch.ms,
transports: switch.transports,
connManager: switch.connManager,
peerStore: switch.peerStore,
dialer: switch.dialer,
nameResolver: switch.nameResolver,
services: switch.services,
connectStub: connectStub)
method connect*(
self: SwitchStub,
peerId: PeerId,
addrs: seq[MultiAddress],
forceDial = false,
reuseConnection = true,
upgradeDir = Direction.Out) {.async.} =
if (self.connectStub != nil):
await self.connectStub()
else:
await self.switch.connect(peerId, addrs, forceDial, reuseConnection, upgradeDir)
import ./stubs/switchstub
suite "Dcutr":
teardown:

167
tests/testhpservice.nim Normal file
View File

@ -0,0 +1,167 @@
# Nim-LibP2P
# Copyright (c) 2022 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.
{.used.}
when (NimMajor, NimMinor) < (1, 4):
{.push raises: [Defect].}
else:
{.push raises: [].}
import chronos
import unittest2
import ./helpers
import ./stubs/switchstub
import ../libp2p/[builders,
switch,
services/hpservice,
services/autorelayservice]
import ../libp2p/protocols/connectivity/relay/[relay, client]
import ../libp2p/protocols/connectivity/autonat/[service]
import ../libp2p/wire
import stubs/autonatclientstub
proc isPublicAddrIPAddrMock(ta: TransportAddress): bool =
return true
proc createSwitch(r: Relay = nil, hpService: Service = nil): Switch {.raises: [LPError, Defect].} =
var builder = SwitchBuilder.new()
.withRng(newRng())
.withAddresses(@[ MultiAddress.init("/ip4/0.0.0.0/tcp/0").tryGet() ])
.withTcpTransport()
.withMplex()
.withAutonat()
.withNoise()
if hpService != nil:
builder = builder.withServices(@[hpService])
if r != nil:
builder = builder.withCircuitRelay(r)
return builder.build()
proc buildRelayMA(switchRelay: Switch, switchClient: Switch): MultiAddress =
MultiAddress.init($switchRelay.peerInfo.addrs[0] & "/p2p/" &
$switchRelay.peerInfo.peerId & "/p2p-circuit/p2p/" &
$switchClient.peerInfo.peerId).get()
suite "Hole Punching":
teardown:
checkTrackers()
asyncTest "Direct connection must work when peer address is public":
let autonatClientStub = AutonatClientStub.new(expectedDials = 1)
autonatClientStub.answer = NotReachable
let autonatService = AutonatService.new(autonatClientStub, newRng(), maxQueueSize = 1)
let relayClient = RelayClient.new()
let privatePeerRelayAddr = newFuture[seq[MultiAddress]]()
let publicPeerSwitch = createSwitch(RelayClient.new())
proc checkMA(address: seq[MultiAddress]) =
if not privatePeerRelayAddr.completed():
privatePeerRelayAddr.complete(address)
let autoRelayService = AutoRelayService.new(1, relayClient, checkMA, newRng())
let hpservice = HPService.new(autonatService, autoRelayService, isPublicAddrIPAddrMock)
let privatePeerSwitch = createSwitch(relayClient, hpservice)
let switchRelay = createSwitch(Relay.new())
await allFutures(switchRelay.start(), privatePeerSwitch.start(), publicPeerSwitch.start())
await privatePeerSwitch.connect(switchRelay.peerInfo.peerId, switchRelay.peerInfo.addrs)
await publicPeerSwitch.connect(privatePeerSwitch.peerInfo.peerId, (await privatePeerRelayAddr))
checkExpiring:
privatePeerSwitch.connManager.connCount(publicPeerSwitch.peerInfo.peerId) == 1 and
not isRelayed(privatePeerSwitch.connManager.selectMuxer(publicPeerSwitch.peerInfo.peerId).connection)
await allFuturesThrowing(
privatePeerSwitch.stop(), publicPeerSwitch.stop(), switchRelay.stop())
proc holePunchingTest(connectStub: proc (): Future[void] {.async.},
isPublicIPAddrProc: IsPublicIPAddrProc,
answer: Answer) {.async.} =
# There's no check in this test cause it can't test hole punching locally. It exists just to be sure the rest of
# the code works properly.
let autonatClientStub1 = AutonatClientStub.new(expectedDials = 1)
autonatClientStub1.answer = NotReachable
let autonatService1 = AutonatService.new(autonatClientStub1, newRng(), maxQueueSize = 1)
let autonatClientStub2 = AutonatClientStub.new(expectedDials = 1)
autonatClientStub2.answer = answer
let autonatService2 = AutonatService.new(autonatClientStub2, newRng(), maxQueueSize = 1)
let relayClient1 = RelayClient.new()
let relayClient2 = RelayClient.new()
let privatePeerRelayAddr1 = newFuture[seq[MultiAddress]]()
proc checkMA(address: seq[MultiAddress]) =
if not privatePeerRelayAddr1.completed():
privatePeerRelayAddr1.complete(address)
let autoRelayService1 = AutoRelayService.new(1, relayClient1, checkMA, newRng())
let autoRelayService2 = AutoRelayService.new(1, relayClient2, nil, newRng())
let hpservice1 = HPService.new(autonatService1, autoRelayService1, isPublicIPAddrProc)
let hpservice2 = HPService.new(autonatService2, autoRelayService2)
let privatePeerSwitch1 = SwitchStub.new(createSwitch(relayClient1, hpservice1))
let privatePeerSwitch2 = createSwitch(relayClient2, hpservice2)
let switchRelay = createSwitch(Relay.new())
let switchAux = createSwitch()
let switchAux2 = createSwitch()
let switchAux3 = createSwitch()
let switchAux4 = createSwitch()
var awaiter = newFuture[void]()
await allFutures(
switchRelay.start(), privatePeerSwitch1.start(), privatePeerSwitch2.start(),
switchAux.start(), switchAux2.start(), switchAux3.start(), switchAux4.start()
)
await privatePeerSwitch1.connect(switchRelay.peerInfo.peerId, switchRelay.peerInfo.addrs)
await privatePeerSwitch2.connect(switchAux.peerInfo.peerId, switchAux.peerInfo.addrs)
await sleepAsync(200.millis)
await privatePeerSwitch1.connect(switchAux2.peerInfo.peerId, switchAux2.peerInfo.addrs)
await privatePeerSwitch1.connect(switchAux3.peerInfo.peerId, switchAux3.peerInfo.addrs)
await privatePeerSwitch1.connect(switchAux4.peerInfo.peerId, switchAux4.peerInfo.addrs)
await privatePeerSwitch2.connect(switchAux2.peerInfo.peerId, switchAux2.peerInfo.addrs)
await privatePeerSwitch2.connect(switchAux3.peerInfo.peerId, switchAux3.peerInfo.addrs)
await privatePeerSwitch2.connect(switchAux4.peerInfo.peerId, switchAux4.peerInfo.addrs)
privatePeerSwitch1.connectStub = connectStub
await privatePeerSwitch2.connect(privatePeerSwitch1.peerInfo.peerId, (await privatePeerRelayAddr1))
await sleepAsync(200.millis)
await allFuturesThrowing(
privatePeerSwitch1.stop(), privatePeerSwitch2.stop(), switchRelay.stop(),
switchAux.stop(), switchAux2.stop(), switchAux3.stop(), switchAux4.stop())
asyncTest "Hole punching when peers addresses are private":
await holePunchingTest(nil, isGlobal, NotReachable)
asyncTest "Hole punching when there is an error during unilateral direct connection":
proc connectStub(): Future[void] {.async.} =
raise newException(CatchableError, "error")
await holePunchingTest(connectStub, isPublicAddrIPAddrMock, Reachable)

View File

@ -44,4 +44,5 @@ import testtcptransport,
testautonat,
testautonatservice,
testautorelay,
testdcutr
testdcutr,
testhpservice