Skip to content

Harden the P2P layer for open (permissionless) networks; 0.1.7 and 0.1.8 - #31

Open
xdagx wants to merge 11 commits into
XDagger:developfrom
xdagx:security/open-network-hardening
Open

xdagx wants to merge 11 commits into
XDagger:developfrom
xdagx:security/open-network-hardening

Conversation

@xdagx

@xdagx xdagx commented Oct 1, 2026 •

Copy link
Copy Markdown

Summary

The P2P layer, hardened so that it can face peers nobody vouches for. This is the precondition for removing the node whitelist in xdagj: XDagger/xdagj#363 depends on this pull request.

Two versions in one branch:

  • 0.1.7 - security hardening. The wire protocol changes (frame version 2, mutual handshake, signed discovery): 0.1.7 and later do not talk to 0.1.6 and earlier.
  • 0.1.8 - two start-up fixes found while testing xdagj on top of 0.1.7. Same on the wire as 0.1.7.

CHANGELOG.md has the full list. The reasoning behind each change is in xdagj's docs/SECURITY_AUDIT_OPEN_NETWORK.md (findings P1-P16), which is part of XDagger/xdagj#363.

0.1.7

  • Signed discovery with endpoint proof. Every discovery datagram is signed with the node key; the sender's identity is recovered from the signature and has to match the id the packet claims. A node enters the table, is dialled, or gets FIND_NODE answered only after it answered one of our pings.
  • Mutual handshake. Both sides issue a nonce; a recorded handshake cannot be replayed; self-connections are detected.
  • Admission control before any handler sees a byte: bans enforced on inbound connections; limits on total, inbound, pending-handshake, per-IP and per-subnet connections; a closed-network mode in which only configured peers are talked to (P2pService.setPermissionless).
  • Bounds and backpressure. Counts read off the wire are checked before anything is allocated; strict frame codec; rate limits before signature verification; inbound byte-rate guard and outbound queue bound.
  • Fixes: ban duration overflow, Channel.send(Bytes) writing unframed bytes, node ids of discovery and handshake never matching (which defeated duplicate-connection detection), getConnectableNodes() returning unverified nodes, PeerClient.connect(host, port) blocking until the connection closed.
  • ReputationManager no longer uses Java serialisation; P2pConfig performs no network access on construction.

0.1.8

  • P2pService.start() returns when the TCP listener and the discovery socket are bound. It used to return while they were still coming up on their own threads: a node started right after another dialled too early and connected 30 s later, and an early stop() left the socket open for the life of the process.
  • P2pService.setPermissionless() may be called before start(); it then only changes the setting (it used to run the connect loop on a service that was not running: a NullPointerException, logged as a warning).

Compatibility

  • Not wire-compatible with 0.1.6 (see above). Applications define their messages at codes 0x20 and above, as before.
  • New configuration (all optional, defaults keep the node open): permissionless, bindIp, connection limits, allowPrivateAddresses; see the "Added" section of the changelog.

Testing

  • mvn test: 923 tests, 0 failures. Two tests need internet access and are skipped without it.
  • New adversarial tests: signed / tampered / foreign-network / unsigned datagrams, oversized packets, neighbour counts on the wire, interleaved and overlong frames, handshake replay and identity binding, rate limits, table limits; two services on the loopback interface for the start-up fixes.
  • Coverage measured for this branch (JaCoCo, example package excluded): 75.5% of instructions, 59.8% of branches. The README carries the new figures; they are lower than before because the code grew by about a quarter.
  • mvn license:check passes.

Notes for reviewers

🤖 Generated with Claude Code

dependabot Bot and others added 11 commits January 19, 2026 01:17
…rectory with 3 updates

Bumps the production-dependencies group with 3 updates in the / directory: [io.netty:netty-all](https://github.com/netty/netty), [dnsjava:dnsjava](https://github.com/dnsjava/dnsjava) and software.amazon.awssdk:route53.


Updates `io.netty:netty-all` from 4.2.8.Final to 4.2.9.Final
- [Commits](netty/netty@netty-4.2.8.Final...netty-4.2.9.Final)

Updates `dnsjava:dnsjava` from 3.6.3 to 3.6.4
- [Release notes](https://github.com/dnsjava/dnsjava/releases)
- [Changelog](https://github.com/dnsjava/dnsjava/blob/master/Changelog)
- [Commits](dnsjava/dnsjava@v3.6.3...v3.6.4)

Updates `software.amazon.awssdk:route53` from 2.40.8 to 2.41.10

---
updated-dependencies:
- dependency-name: io.netty:netty-all
  dependency-version: 4.2.9.Final
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: dnsjava:dnsjava
  dependency-version: 3.6.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: software.amazon.awssdk:route53
  dependency-version: 2.41.10
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
… 1 directory with 7 updates

Bumps the development-dependencies group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| software.amazon.awssdk:apache-client | `2.40.8` | `2.41.34` |
| [org.junit.jupiter:junit-jupiter](https://github.com/junit-team/junit-framework) | `6.0.1` | `6.0.3` |
| [org.mockito:mockito-core](https://github.com/mockito/mockito) | `5.20.0` | `5.21.0` |
| [org.mockito:mockito-junit-jupiter](https://github.com/mockito/mockito) | `5.20.0` | `5.21.0` |
| [org.apache.maven.plugins:maven-compiler-plugin](https://github.com/apache/maven-compiler-plugin) | `3.14.1` | `3.15.0` |
| [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) | `3.5.4` | `3.5.5` |
| [org.sonatype.central:central-publishing-maven-plugin](https://github.com/sonatype/central-publishing-maven-plugin) | `0.9.0` | `0.10.0` |



Updates `software.amazon.awssdk:apache-client` from 2.40.8 to 2.41.34

Updates `org.junit.jupiter:junit-jupiter` from 6.0.1 to 6.0.3
- [Release notes](https://github.com/junit-team/junit-framework/releases)
- [Commits](junit-team/junit-framework@r6.0.1...r6.0.3)

Updates `org.mockito:mockito-core` from 5.20.0 to 5.21.0
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](mockito/mockito@v5.20.0...v5.21.0)

Updates `org.mockito:mockito-junit-jupiter` from 5.20.0 to 5.21.0
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](mockito/mockito@v5.20.0...v5.21.0)

Updates `org.mockito:mockito-junit-jupiter` from 5.20.0 to 5.21.0
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](mockito/mockito@v5.20.0...v5.21.0)

Updates `org.apache.maven.plugins:maven-compiler-plugin` from 3.14.1 to 3.15.0
- [Release notes](https://github.com/apache/maven-compiler-plugin/releases)
- [Commits](apache/maven-compiler-plugin@maven-compiler-plugin-3.14.1...maven-compiler-plugin-3.15.0)

Updates `org.apache.maven.plugins:maven-surefire-plugin` from 3.5.4 to 3.5.5
- [Release notes](https://github.com/apache/maven-surefire/releases)
- [Commits](apache/maven-surefire@surefire-3.5.4...surefire-3.5.5)

Updates `org.sonatype.central:central-publishing-maven-plugin` from 0.9.0 to 0.10.0
- [Commits](https://github.com/sonatype/central-publishing-maven-plugin/commits)

---
updated-dependencies:
- dependency-name: software.amazon.awssdk:apache-client
  dependency-version: 2.41.34
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: org.junit.jupiter:junit-jupiter
  dependency-version: 6.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: org.mockito:mockito-core
  dependency-version: 5.21.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: org.mockito:mockito-junit-jupiter
  dependency-version: 5.21.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: org.mockito:mockito-junit-jupiter
  dependency-version: 5.21.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: org.apache.maven.plugins:maven-compiler-plugin
  dependency-version: 3.15.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: org.apache.maven.plugins:maven-surefire-plugin
  dependency-version: 3.5.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: org.sonatype.central:central-publishing-maven-plugin
  dependency-version: 0.10.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v5...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
…actions/upload-artifact-7

chore(ci)(deps): Bump actions/upload-artifact from 5 to 7
…nt-dependencies-20e0884118

chore(deps)(deps-dev): Bump the development-dependencies group across 1 directory with 7 updates
…n-dependencies-9d66e4106a

chore(deps)(deps): Bump the production-dependencies group across 1 directory with 3 updates
The library is now safe to expose to peers nobody vouches for. The wire
protocol changes (frame version 2, mutual handshake, signed discovery):
0.1.7 nodes do not talk to 0.1.6 nodes.

Security
- Signed discovery (KadPacket): every UDP datagram is signed with the
  node key over sha256("xdag-discovery-v2" | networkId | code | body);
  the sender's identity is recovered from the signature and must match
  the id the packet claims. Addresses a node claims for itself are
  ignored: a node is reached where its datagrams come from.
- Endpoint proof: a node enters the table, is dialled or gets FIND_NODE
  answered only after it answered a ping of ours with a pong that
  echoes the hash of that ping; bonds expire after 12 h. Neighbour
  answers are sent in datagrams of at most 8 nodes (1280-byte limit)
  and accepted for 5 s after the question, up to a bucket's worth.
- Mutual handshake: INIT both ways; HELLO signs the dialler's nonce,
  WORLD signs sha256(acceptor nonce | acceptor id). A recorded WORLD
  cannot be replayed; self-connections are detected.
- Admission control before any handler sees a byte: bans enforced on
  inbound connections, closed-network mode (configured peers only),
  limits on total / inbound / pending-handshake / per-IP / per-subnet
  connections, handshake timeout.
- Bounds on everything read off the wire (SimpleDecoder.readCount):
  neighbours <= 16, capabilities <= 32, DNS nodes <= 1024; strict frame
  codec (no resync on garbage); one packet in flight per connection;
  per-source rate limit before signature verification; bounded numbers
  of tracked nodes per IP / subnet / unverified / total; table limits.
- Backpressure: inbound byte rate guard, outbound queue bound, Netty
  water marks, Channel.isWritable() / onWritabilityChanged.
- Host strings from other nodes are never resolved; ReputationManager
  without Java serialisation and bounded; no network access when a
  P2pConfig is created (external IP detection on request, HTTPS only).

Fixed
- Ban duration overflow after ~48 offences.
- Channel.send(Bytes) wrote unframed bytes to the socket.
- Node ids in discovery (hex) and in the handshake (Base58) never
  matched; peers are now identified by the listen address they announce
  (the loopback heuristic kept a node from connecting to a second
  neighbour on one machine).
- getConnectableNodes() returned unverified nodes.
- PeerClient.connect() blocked until the connection closed; stop()
  without start() threw.
- Remote-triggered log lines downgraded; tests hermetic (no leaked
  ports, no internet).

Added
- P2pService.setPermissionless(boolean) to open or close the network at
  run time, and the matching P2pConfig settings.

910 tests pass (2 skipped: they need the external-IP services).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…art() only changes the setting

Two start-up problems found while testing xdagj on top of 0.1.7.

- P2pService.start() returned while the TCP listener (PeerServer) and the
  discovery socket (DiscoverServer) were still being bound on their own
  threads. A node started right after another one dialled it too early
  and was refused - and an address that failed is not dialled again for
  30 s, so the two connected half a minute later. stop() / close() called
  that early found nothing to close, and the socket stayed open for the
  life of the process (PeerServerTest left a listener on port 8080 that
  way).
  PeerServer.start() and DiscoverServer.init() now return when the socket
  is bound, or when it is clear that it cannot be: that is logged and the
  service runs without it (PeerServer.isListening(),
  NodeManager.isDiscoveryListening()). A stop that comes while a socket
  is being bound closes it as soon as it is. start() twice starts once;
  start() and stop() exclude each other.
- P2pService.setPermissionless(true) before start() ran the connect loop
  on a service that had neither a node table nor a client yet: a
  NullPointerException, caught and logged as a warning at every start of
  a node that begins in open mode. On a service that is not running only
  the setting changes; ChannelManager.triggerImmediateConnect() does
  nothing before start and after stop; NodeManager returns empty node
  lists before init() instead of throwing.

Tests: 923 pass, 2 skipped (they need the internet). New:
P2pServiceStartTest (two services on 127.0.0.1 connect at once; both
sockets bound on return; mode set before start; start twice),
DiscoverServerTest, PeerServerTest (bound on return, port free after
stop, port taken), ChannelManagerTest (the trigger only works while
running), P2pServiceTest (mode before start and after stop, node lists
before start). With either fix disabled, its tests fail.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The start-up fixes of the previous commit (start() returns with the
sockets bound; setPermissionless() before start() only changes the
setting) are released as 0.1.8 instead of being added to 0.1.7, so that a
build with them can be told from one without. Nothing changes on the
wire: 0.1.8 and 0.1.7 nodes talk to each other.

- pom.xml, default client id: 0.1.8.
- CHANGELOG: the two fixes move from 0.1.7 to a 0.1.8 section.
- README and guides: dependency snippet and jar names (they still said
  0.1.6), what is new in 0.1.8 and 0.1.7 - including that 0.1.7 and later
  do not talk to 0.1.6 -, test count.

Tests: 923 pass, 2 skipped (they need the internet).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The figures were those of 0.1.3 (76% of 15,810 instructions). Measured
again with JaCoCo 0.8.14 (mvn test, example package excluded as before),
three runs:

  instructions  75.5%  (15,100-15,106 of 19,994)
  branches      59.8%  (1,366-1,367 of 2,286)
  lines         75.5%  (3,396-3,397 of 4,500)
  methods       84.5%  (580 of 686), classes 90 of 95

The code grew by a quarter with the hardening of 0.1.7, and its branches
are covered less well than the rest was. The runs were offline: the two
tests that need the internet were skipped, which leaves the external-IP
lookup uncovered. The suite takes about a minute and a half, not thirty
seconds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants