diff --git a/libp2p/builders.nim b/libp2p/builders.nim index b459a9a9c4..828fc5283f 100644 --- a/libp2p/builders.nim +++ b/libp2p/builders.nim @@ -26,7 +26,7 @@ import switch, peerid, peerinfo, stream/connection, multiaddress, crypto/crypto, transports/[transport, tcptransport], muxers/[muxer, mplex/mplex, yamux/yamux], - protocols/[identify, secure/secure, secure/noise], + protocols/[identify, secure/secure, secure/noise, autonat], protocols/relay/[relay, client, rtransport], connmanager, upgrademngrs/muxedupgrade, nameresolving/nameresolver, @@ -58,6 +58,7 @@ type agentVersion: string nameResolver: NameResolver peerStoreCapacity: Option[int] + autonat: bool circuitRelay: Relay proc new*(T: type[SwitchBuilder]): T {.public.} = @@ -185,6 +186,10 @@ proc withNameResolver*(b: SwitchBuilder, nameResolver: NameResolver): SwitchBuil b.nameResolver = nameResolver b +proc withAutonat*(b: SwitchBuilder): SwitchBuilder = + b.autonat = true + b + proc withCircuitRelay*(b: SwitchBuilder, r: Relay = Relay.new()): SwitchBuilder = b.circuitRelay = r b @@ -246,6 +251,10 @@ proc build*(b: SwitchBuilder): Switch nameResolver = b.nameResolver, peerStore = peerStore) + if b.autonat: + let autonat = Autonat.new(switch) + switch.mount(autonat) + if not isNil(b.circuitRelay): if b.circuitRelay of RelayClient: switch.addTransport(RelayTransport.new(RelayClient(b.circuitRelay), muxedUpgrade)) diff --git a/libp2p/dial.nim b/libp2p/dial.nim index 46843e56f0..bfe1620f53 100644 --- a/libp2p/dial.nim +++ b/libp2p/dial.nim @@ -58,3 +58,9 @@ method addTransport*( self: Dial, transport: Transport) {.base.} = doAssert(false, "Not implemented!") + +method tryDial*( + self: Dial, + peerId: PeerId, + addrs: seq[MultiAddress]): Future[MultiAddress] {.async, base.} = + doAssert(false, "Not implemented!") diff --git a/libp2p/dialer.nim b/libp2p/dialer.nim index ece835af70..05438d04b7 100644 --- a/libp2p/dialer.nim +++ b/libp2p/dialer.nim @@ -21,6 +21,7 @@ import dial, stream/connection, transports/transport, nameresolving/nameresolver, + upgrademngrs/upgrade, errors export dial, errors @@ -178,6 +179,27 @@ proc negotiateStream( return conn +method tryDial*( + self: Dialer, + peerId: PeerId, + addrs: seq[MultiAddress]): Future[MultiAddress] {.async.} = + ## Create a protocol stream and in order to check + ## if a connection is possible. + ## Doesn't use the Connection Manager to save it. + ## + + trace "Check if it can dial", peerId, addrs + try: + let conn = await self.dialAndUpgrade(peerId, addrs) + if conn.isNil(): + raise newException(DialFailedError, "No valid multiaddress") + await conn.close() + return conn.observedAddr + except CancelledError as exc: + raise exc + except CatchableError as exc: + raise newException(DialFailedError, exc.msg) + method dial*( self: Dialer, peerId: PeerId, diff --git a/libp2p/multiaddress.nim b/libp2p/multiaddress.nim index 9485488149..9fd636c200 100644 --- a/libp2p/multiaddress.nim +++ b/libp2p/multiaddress.nim @@ -590,10 +590,28 @@ proc getPart(ma: MultiAddress, index: int): MaResult[MultiAddress] = inc(offset) ok(res) +proc getParts[U, V](ma: MultiAddress, slice: HSlice[U, V]): MaResult[MultiAddress] = + when slice.a is BackwardsIndex or slice.b is BackwardsIndex: + let maLength = ? len(ma) + template normalizeIndex(index): int = + when index is BackwardsIndex: maLength - int(index) + else: int(index) + let + indexStart = normalizeIndex(slice.a) + indexEnd = normalizeIndex(slice.b) + var res: MultiAddress + for i in indexStart..indexEnd: + ? res.append(? ma[i]) + ok(res) + proc `[]`*(ma: MultiAddress, i: int): MaResult[MultiAddress] {.inline.} = ## Returns part with index ``i`` of MultiAddress ``ma``. ma.getPart(i) +proc `[]`*(ma: MultiAddress, slice: HSlice): MaResult[MultiAddress] {.inline.} = + ## Returns parts with slice ``slice`` of MultiAddress ``ma``. + ma.getParts(slice) + iterator items*(ma: MultiAddress): MaResult[MultiAddress] = ## Iterates over all addresses inside of MultiAddress ``ma``. var header: uint64 @@ -630,6 +648,13 @@ iterator items*(ma: MultiAddress): MaResult[MultiAddress] = res.data.finish() yield ok(MaResult[MultiAddress], res) +proc len*(ma: MultiAddress): MaResult[int] = + var counter: int + for part in ma: + if part.isErr: return err(part.error) + counter.inc() + ok(counter) + proc contains*(ma: MultiAddress, codec: MultiCodec): MaResult[bool] {.inline.} = ## Returns ``true``, if address with MultiCodec ``codec`` present in ## MultiAddress ``ma``. diff --git a/libp2p/protocols/autonat.nim b/libp2p/protocols/autonat.nim new file mode 100644 index 0000000000..bdee799905 --- /dev/null +++ b/libp2p/protocols/autonat.nim @@ -0,0 +1,301 @@ +# 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/[options, sets, sequtils] +import chronos, chronicles, stew/objects +import ./protocol, + ../switch, + ../multiaddress, + ../multicodec, + ../peerid, + ../utils/semaphore, + ../errors + +logScope: + topics = "libp2p autonat" + +const + AutonatCodec* = "/libp2p/autonat/1.0.0" + AddressLimit = 8 + +type + AutonatError* = object of LPError + + MsgType* = enum + Dial = 0 + DialResponse = 1 + + ResponseStatus* = enum + Ok = 0 + DialError = 100 + DialRefused = 101 + BadRequest = 200 + InternalError = 300 + + AutonatPeerInfo* = object + id: Option[PeerId] + addrs: seq[MultiAddress] + + AutonatDial* = object + peerInfo: Option[AutonatPeerInfo] + + AutonatDialResponse* = object + status*: ResponseStatus + text*: Option[string] + ma*: Option[MultiAddress] + + AutonatMsg = object + msgType: MsgType + dial: Option[AutonatDial] + response: Option[AutonatDialResponse] + +proc encode*(msg: AutonatMsg): ProtoBuffer = + result = initProtoBuffer() + result.write(1, msg.msgType.uint) + if msg.dial.isSome(): + var dial = initProtoBuffer() + if msg.dial.get().peerInfo.isSome(): + var bufferPeerInfo = initProtoBuffer() + let peerInfo = msg.dial.get().peerInfo.get() + if peerInfo.id.isSome(): + bufferPeerInfo.write(1, peerInfo.id.get()) + for ma in peerInfo.addrs: + bufferPeerInfo.write(2, ma.data.buffer) + bufferPeerInfo.finish() + dial.write(1, bufferPeerInfo.buffer) + dial.finish() + result.write(2, dial.buffer) + if msg.response.isSome(): + var bufferResponse = initProtoBuffer() + let response = msg.response.get() + bufferResponse.write(1, response.status.uint) + if response.text.isSome(): + bufferResponse.write(2, response.text.get()) + if response.ma.isSome(): + bufferResponse.write(3, response.ma.get()) + bufferResponse.finish() + result.write(3, bufferResponse.buffer) + result.finish() + +proc encode*(d: AutonatDial): ProtoBuffer = + result = initProtoBuffer() + result.write(1, MsgType.Dial.uint) + var dial = initProtoBuffer() + if d.peerInfo.isSome(): + var bufferPeerInfo = initProtoBuffer() + let peerInfo = d.peerInfo.get() + if peerInfo.id.isSome(): + bufferPeerInfo.write(1, peerInfo.id.get()) + for ma in peerInfo.addrs: + bufferPeerInfo.write(2, ma.data.buffer) + bufferPeerInfo.finish() + dial.write(1, bufferPeerInfo.buffer) + dial.finish() + result.write(2, dial.buffer) + result.finish() + +proc encode*(r: AutonatDialResponse): ProtoBuffer = + result = initProtoBuffer() + result.write(1, MsgType.DialResponse.uint) + var bufferResponse = initProtoBuffer() + bufferResponse.write(1, r.status.uint) + if r.text.isSome(): + bufferResponse.write(2, r.text.get()) + if r.ma.isSome(): + bufferResponse.write(3, r.ma.get()) + bufferResponse.finish() + result.write(3, bufferResponse.buffer) + result.finish() + +proc decode(_: typedesc[AutonatMsg], buf: seq[byte]): Option[AutonatMsg] = + var + msgTypeOrd: uint32 + pbDial: ProtoBuffer + pbResponse: ProtoBuffer + msg: AutonatMsg + + let + pb = initProtoBuffer(buf) + r1 = pb.getField(1, msgTypeOrd) + r2 = pb.getField(2, pbDial) + r3 = pb.getField(3, pbResponse) + if r1.isErr() or r2.isErr() or r3.isErr(): return none(AutonatMsg) + + if r1.get() and not checkedEnumAssign(msg.msgType, msgTypeOrd): + return none(AutonatMsg) + if r2.get(): + var + pbPeerInfo: ProtoBuffer + dial: AutonatDial + let + r4 = pbDial.getField(1, pbPeerInfo) + if r4.isErr(): return none(AutonatMsg) + + var peerInfo: AutonatPeerInfo + if r4.get(): + var pid: PeerId + let + r5 = pbPeerInfo.getField(1, pid) + r6 = pbPeerInfo.getRepeatedField(2, peerInfo.addrs) + if r5.isErr() or r6.isErr(): return none(AutonatMsg) + if r5.get(): peerInfo.id = some(pid) + dial.peerInfo = some(peerInfo) + msg.dial = some(dial) + + if r3.get(): + var + statusOrd: uint + text: string + ma: MultiAddress + response: AutonatDialResponse + + let + r4 = pbResponse.getField(1, statusOrd) + r5 = pbResponse.getField(2, text) + r6 = pbResponse.getField(3, ma) + + if r4.isErr() or r5.isErr() or r6.isErr() or + (r4.get() and not checkedEnumAssign(response.status, statusOrd)): + return none(AutonatMsg) + if r5.get(): response.text = some(text) + if r6.get(): response.ma = some(ma) + msg.response = some(response) + + return some(msg) + +proc sendDial(conn: Connection, pid: PeerId, addrs: seq[MultiAddress]) {.async.} = + let pb = AutonatDial(peerInfo: some(AutonatPeerInfo( + id: some(pid), + addrs: addrs + ))).encode() + await conn.writeLp(pb.buffer) + +proc sendResponseError(conn: Connection, status: ResponseStatus, text: string = "") {.async.} = + let pb = AutonatDialResponse( + status: status, + text: if text == "": none(string) else: some(text), + ma: none(MultiAddress) + ).encode() + await conn.writeLp(pb.buffer) + +proc sendResponseOk(conn: Connection, ma: MultiAddress) {.async.} = + let pb = AutonatDialResponse( + status: ResponseStatus.Ok, + text: some("Ok"), + ma: some(ma) + ).encode() + await conn.writeLp(pb.buffer) + +type + Autonat* = ref object of LPProtocol + sem: AsyncSemaphore + switch*: Switch + +proc dialMe*(a: Autonat, pid: PeerId, ma: MultiAddress|seq[MultiAddress]): + Future[MultiAddress] {.async.} = + let addrs = when ma is MultiAddress: @[ma] else: ma + let conn = await a.switch.dial(pid, addrs, AutonatCodec) + defer: await conn.close() + await conn.sendDial(a.switch.peerInfo.peerId, a.switch.peerInfo.addrs) + let msgOpt = AutonatMsg.decode(await conn.readLp(1024)) + if msgOpt.isNone() or + msgOpt.get().msgType != DialResponse or + msgOpt.get().response.isNone(): + raise newException(AutonatError, "Unexpected response") + let response = msgOpt.get().response.get() + if response.status != ResponseStatus.Ok: + raise newException(AutonatError, "Bad status " & + $response.status & " " & + response.text.get("")) + if response.ma.isNone(): + raise newException(AutonatError, "Missing address") + return response.ma.get() + +proc tryDial(a: Autonat, conn: Connection, addrs: seq[MultiAddress]) {.async.} = + try: + await a.sem.acquire() + let ma = await a.switch.dialer.tryDial(conn.peerId, addrs) + await conn.sendResponseOk(ma) + except CancelledError as exc: + raise exc + except CatchableError as exc: + await conn.sendResponseError(DialError, exc.msg) + finally: + a.sem.release() + +proc handleDial(a: Autonat, conn: Connection, msg: AutonatMsg): Future[void] = + if msg.dial.isNone() or msg.dial.get().peerInfo.isNone(): + return conn.sendResponseError(BadRequest, "Missing Peer Info") + let peerInfo = msg.dial.get().peerInfo.get() + if peerInfo.id.isSome() and peerInfo.id.get() != conn.peerId: + return conn.sendResponseError(BadRequest, "PeerId mismatch") + + var isRelayed = conn.observedAddr.contains(multiCodec("p2p-circuit")) + if isRelayed.isErr() or isRelayed.get(): + return conn.sendResponseError(DialRefused, "Refused to dial a relayed observed address") + let hostIp = conn.observedAddr[0] + if hostIp.isErr() or not IP.match(hostIp.get()): + trace "wrong observed address", address=conn.observedAddr + return conn.sendResponseError(InternalError, "Expected an IP address") + var addrs = initHashSet[MultiAddress]() + addrs.incl(conn.observedAddr) + for ma in peerInfo.addrs: + isRelayed = ma.contains(multiCodec("p2p-circuit")) + if isRelayed.isErr() or isRelayed.get(): + continue + let maFirst = ma[0] + if maFirst.isErr() or not IP.match(maFirst.get()): + continue + + try: + addrs.incl( + if maFirst.get() == hostIp.get(): + ma + else: + let maEnd = ma[1..^1] + if maEnd.isErr(): continue + hostIp.get() & maEnd.get() + ) + except LPError as exc: + continue + if len(addrs) >= AddressLimit: + break + + if len(addrs) == 0: + return conn.sendResponseError(DialRefused, "No dialable address") + return a.tryDial(conn, toSeq(addrs)) + +proc new*(T: typedesc[Autonat], switch: Switch, semSize: int = 1): T = + let autonat = T(switch: switch, sem: newAsyncSemaphore(semSize)) + autonat.init() + autonat + +method init*(a: Autonat) = + proc handleStream(conn: Connection, proto: string) {.async, gcsafe.} = + try: + let msgOpt = AutonatMsg.decode(await conn.readLp(1024)) + if msgOpt.isNone() or msgOpt.get().msgType != MsgType.Dial: + raise newException(AutonatError, "Received malformed message") + let msg = msgOpt.get() + await a.handleDial(conn, msg) + except CancelledError as exc: + raise exc + except CatchableError as exc: + trace "exception in autonat handler", exc = exc.msg, conn + finally: + trace "exiting autonat handler", conn + await conn.close() + + a.handler = handleStream + a.codec = AutonatCodec diff --git a/tests/testautonat.nim b/tests/testautonat.nim new file mode 100644 index 0000000000..8d523fae48 --- /dev/null +++ b/tests/testautonat.nim @@ -0,0 +1,59 @@ +import std/options +import chronos +import + ../libp2p/[ + builders, + protocols/autonat + ], + ./helpers + +proc createAutonatSwitch(): Switch = + result = SwitchBuilder.new() + .withRng(newRng()) + .withAddresses(@[ MultiAddress.init("/ip4/0.0.0.0/tcp/0").tryGet() ]) + .withTcpTransport() + .withMplex() + .withAutonat() + .withNoise() + .build() + +proc makeAutonatServicePrivate(): Switch = + var autonatProtocol = new LPProtocol + autonatProtocol.handler = proc (conn: Connection, proto: string) {.async, gcsafe.} = + discard await conn.readLp(1024) + await conn.writeLp(AutonatDialResponse( + status: DialError, + text: some("dial failed"), + ma: none(MultiAddress)).encode().buffer) + await conn.close() + autonatProtocol.codec = AutonatCodec + result = newStandardSwitch() + result.mount(autonatProtocol) + +suite "Autonat": + teardown: + checkTrackers() + + asyncTest "Simple test": + let + src = newStandardSwitch() + dst = createAutonatSwitch() + await src.start() + await dst.start() + + await src.connect(dst.peerInfo.peerId, dst.peerInfo.addrs) + let ma = await Autonat.new(src).dialMe(dst.peerInfo.peerId, dst.peerInfo.addrs) + await allFutures(src.stop(), dst.stop()) + + asyncTest "Simple failed test": + let + src = newStandardSwitch() + dst = makeAutonatServicePrivate() + + await src.start() + await dst.start() + + await src.connect(dst.peerInfo.peerId, dst.peerInfo.addrs) + expect AutonatError: + discard await Autonat.new(src).dialMe(dst.peerInfo.peerId, dst.peerInfo.addrs) + await allFutures(src.stop(), dst.stop()) diff --git a/tests/testmultiaddress.nim b/tests/testmultiaddress.nim index c4d09a6ff0..4ce91f7294 100644 --- a/tests/testmultiaddress.nim +++ b/tests/testmultiaddress.nim @@ -367,3 +367,12 @@ suite "MultiAddress test suite": check: MultiAddress.init("/ip4/0.0.0.0").get().protoAddress().get() == address_v4 MultiAddress.init("/ip6/::0").get().protoAddress().get() == address_v6 + + test "MultiAddress getParts": + let ma = MultiAddress.init("/ip4/0.0.0.0/tcp/0/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/p2p-circuit/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSuNEXT/unix/stdio/").get() + check: + $ma[0..0].get() == "/ip4/0.0.0.0" + $ma[0..1].get() == "/ip4/0.0.0.0/tcp/0" + $ma[1..2].get() == "/tcp/0/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC" + $ma[^3..^1].get() == "/p2p-circuit/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSuNEXT/unix/stdio" + ma[5..7].isErr() diff --git a/tests/testnative.nim b/tests/testnative.nim index 4106849bcf..2bad494679 100644 --- a/tests/testnative.nim +++ b/tests/testnative.nim @@ -36,4 +36,6 @@ import testtcptransport, testping, testmplex, testrelayv1, - testrelayv2 + testrelayv2, + testyamux, + testautonat