diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b41e4336..c89445e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,12 +15,12 @@ jobs: name: Lint runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v5 with: - go-version: 1.18 - - uses: actions/checkout@v3 + go-version: stable + - uses: actions/checkout@v4 - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: args: --issues-exit-code=1 @@ -34,7 +34,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v2 @@ -51,17 +51,17 @@ jobs: strategy: fail-fast: false matrix: - goversion: ["1.17", "1.18"] + goversion: ["1.22", "1.23", "1.24"] name: Build & Test (Linux, Go ${{ matrix.goversion }}) needs: [lint] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.goversion }} @@ -75,17 +75,17 @@ jobs: strategy: fail-fast: false matrix: - goversion: ["1.17", "1.18"] + goversion: ["1.22", "1.23", "1.24"] name: Build & Test (Windows, Go ${{ matrix.goversion }}) needs: [lint] runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.goversion }} @@ -99,17 +99,17 @@ jobs: strategy: fail-fast: false matrix: - goversion: ["1.17", "1.18"] + goversion: ["1.22", "1.23", "1.24"] name: Build & Test (macOS, Go ${{ matrix.goversion }}) needs: [lint] runs-on: macos-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.goversion }} @@ -119,6 +119,32 @@ jobs: - name: Unit tests run: go test -v ./... + build-freebsd: + strategy: + fail-fast: false + matrix: + goversion: ["1.22", "1.23", "1.24"] + goos: + - freebsd + - openbsd + + name: Build (Cross ${{ matrix.goos }}, Go ${{ matrix.goversion }}) + needs: [lint] + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.goversion }} + + - name: Build Yggdrasil + run: go build -v ./... + env: + GOOS: ${{ matrix.goos }} + tests-ok: name: All tests passed needs: [lint, codeql, build-linux, build-windows, build-macos] @@ -129,135 +155,3 @@ jobs: uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} - - build-packages-debian: - strategy: - fail-fast: false - matrix: - pkgarch: ["amd64", "i386", "mips", "mipsel", "armhf", "armel", "arm64"] - - name: Create Package (Debian, ${{ matrix.pkgarch }}) - needs: [tests-ok] - - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v3 - with: - go-version: 1.18 - - - name: Build package - env: - PKGARCH: ${{ matrix.pkgarch }} - run: sh contrib/deb/generate.sh - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - with: - name: Debian package (${{ matrix.pkgarch }}) - path: "*.deb" - if-no-files-found: error - - build-packages-macos: - strategy: - fail-fast: false - matrix: - pkgarch: ["amd64", "arm64"] - - name: Create Package (macOS, ${{ matrix.pkgarch }}) - needs: [tests-ok] - - runs-on: macos-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v3 - with: - go-version: 1.18 - - - name: Build package - env: - PKGARCH: ${{ matrix.pkgarch }} - run: sh contrib/macos/create-pkg.sh - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - with: - name: macOS package (${{ matrix.pkgarch }}) - path: "*.pkg" - if-no-files-found: error - - build-packages-windows: - strategy: - fail-fast: false - matrix: - pkgarch: ["x64", "x86", "arm", "arm64"] - - name: Create Package (Windows, ${{ matrix.pkgarch }}) - needs: [tests-ok] - - runs-on: windows-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v3 - with: - go-version: 1.18 - - - name: Build package - run: sh contrib/msi/build-msi.sh ${{ matrix.pkgarch }} - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - with: - name: Windows package (${{ matrix.pkgarch }}) - path: "*.msi" - if-no-files-found: error - - build-packages-router: - strategy: - fail-fast: false - matrix: - pkgarch: ["edgerouter-x", "edgerouter-lite", "vyos-amd64", "vyos-i386"] - - name: Create Package (Router, ${{ matrix.pkgarch }}) - needs: [tests-ok] - - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - path: yggdrasil - - - uses: actions/checkout@v3 - with: - repository: neilalexander/vyatta-yggdrasil - path: vyatta-yggdrasil - - - name: Set up Go - uses: actions/setup-go@v3 - with: - go-version: 1.18 - - - name: Build package - env: - BUILDDIR_YGG: /home/runner/work/yggdrasil-go/yggdrasil-go/yggdrasil - run: cd /home/runner/work/yggdrasil-go/yggdrasil-go/vyatta-yggdrasil && ./build-${{ matrix.pkgarch }} - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - with: - name: Router package (${{ matrix.pkgarch }}) - path: "/home/runner/work/yggdrasil-go/yggdrasil-go/vyatta-yggdrasil/*.deb" - if-no-files-found: error diff --git a/.github/workflows/pkg.yml b/.github/workflows/pkg.yml new file mode 100644 index 00000000..1484618e --- /dev/null +++ b/.github/workflows/pkg.yml @@ -0,0 +1,140 @@ +name: Packages + +on: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-packages-debian: + strategy: + fail-fast: false + matrix: + pkgarch: ["amd64", "i386", "mips", "mipsel", "armhf", "armel", "arm64"] + + name: Package (Debian, ${{ matrix.pkgarch }}) + + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "stable" + + - name: Build package + env: + PKGARCH: ${{ matrix.pkgarch }} + run: sh contrib/deb/generate.sh + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: Debian package (${{ matrix.pkgarch }}) + path: "*.deb" + if-no-files-found: error + + build-packages-macos: + strategy: + fail-fast: false + matrix: + pkgarch: ["amd64", "arm64"] + + name: Package (macOS, ${{ matrix.pkgarch }}) + + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "stable" + + - name: Build package + env: + PKGARCH: ${{ matrix.pkgarch }} + run: sh contrib/macos/create-pkg.sh + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: macOS package (${{ matrix.pkgarch }}) + path: "*.pkg" + if-no-files-found: error + + build-packages-windows: + strategy: + fail-fast: false + matrix: + pkgarch: ["x64", "x86", "arm", "arm64"] + + name: Package (Windows, ${{ matrix.pkgarch }}) + + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "stable" + + - name: Setup .NET Core SDK + uses: actions/setup-dotnet@v4 + + - name: Build package + run: sh contrib/msi/build-msi.sh ${{ matrix.pkgarch }} + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: Windows package (${{ matrix.pkgarch }}) + path: "*.msi" + if-no-files-found: error + + build-packages-router: + strategy: + fail-fast: false + matrix: + pkgarch: ["edgerouter-x", "edgerouter-lite", "vyos-amd64", "vyos-i386"] + + name: Package (Router, ${{ matrix.pkgarch }}) + + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + path: yggdrasil + + - uses: actions/checkout@v4 + with: + repository: neilalexander/vyatta-yggdrasil + path: vyatta-yggdrasil + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "stable" + + - name: Build package + env: + BUILDDIR_YGG: /home/runner/work/yggdrasil-go/yggdrasil-go/yggdrasil + run: cd /home/runner/work/yggdrasil-go/yggdrasil-go/vyatta-yggdrasil && ./build-${{ matrix.pkgarch }} + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: Router package (${{ matrix.pkgarch }}) + path: "/home/runner/work/yggdrasil-go/yggdrasil-go/vyatta-yggdrasil/*.deb" + if-no-files-found: error diff --git a/.golangci.yml b/.golangci.yml index c35edee4..836af618 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -2,9 +2,10 @@ run: build-tags: - lint issues-exit-code: 0 # TODO: change this to 1 when we want it to fail builds - skip-dirs: +issues: + exclude-dirs: - contrib/ - misc/ linters: disable: - - gocyclo \ No newline at end of file + - gocyclo diff --git a/CHANGELOG.md b/CHANGELOG.md index 34001548..c7ac1d80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,651 +26,930 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - in case of vulnerabilities. --> +## [0.5.12] - 2024-12-18 + +* Go 1.22 is now required to build Yggdrasil + +### Changed + +* The `latency_ms` field in the admin socket `getPeers` response has been renamed to `latency` + +### Fixed + +* A timing regression which causes a higher level of idle protocol traffic on each peering has been fixed +* The `-user` flag now correctly detects an empty user/group specification + +## [0.5.11] - 2024-12-12 + +### Added + +* Support for `unveil` and `pledge` on OpenBSD + +### Changed + +* The parent selection algorithm now only chooses a new parent if there is a larger cost benefit to doing so, which should help to stabilise the tree +* The bloom filters are now repropagated periodically, to avoid nodes getting stuck with bad state + +### Fixed + +* A memory leak caused by missed cleanup of the peer response map has been fixed +* Other bug fixes with bloom filter propagation for off-tree filters and zero vs one bits +* TLS-based peering connections now support TLS 1.2 again + +## [0.5.10] - 2024-11-24 + +### Added + +* The `getPeers` admin endpoint will now report the current transmit/receive rate for each given peer +* The `getMulticastInterfaces` admin endpoint now reports much more useful information about each interface, rather than just a list of interface names + +### Changed + +* Minor tweaks to the routing algorithm: + * The next-hop selection will now prefer shorter paths when the costed distance is otherwise equal, tiebreaking on peering uptime to fall back to more stable paths + * Link cost calculations have been smoothed out, making the costs less sensitive to sudden spikes in latency +* Reusable name lookup and peer connection logic across different peering types for more consistent behaviour +* Some comments in the configuration file have been revised for clarity +* Upgrade dependencies + +### Fixed + +* Nodes with `IfName` set to `none` will now correctly respond to debug RPC requests +* The admin socket will now be created reliably before dropping privileges with `-user` +* Clear supplementary groups when providing a group ID as well as a user ID to `-user` +* SOCKS and WebSocket peerings should now use the correct source interface when specified in `InterfacePeers` +* `Peers` and `InterfacePeers` addresses that are obviously invalid (such as unspecified or multicast addresses) will now be correctly ignored +* Listeners should now shut down correctly, which should resolve issues where multicast listeners for specific interfaces would not come back up or would log errors + +## [0.5.9] - 2024-10-19 + +### Added + +* New command line option `-user` for changing the process UID/GID + +### Changed + +* The routing algorithm has been updated with RTT-aware link costing, which should prefer lower latency links over higher latency links where possible + * The calculated cost is an average of the link RTT, but newly established links are costed higher to begin with, such that unstable peerings can be avoided + * Link costs are only used where multiple next-hops are available and will be ignored if there is only one loop-free path to the destination + * This is protocol-compatible with existing v0.5.x nodes but will have the best results when peering with nodes that are also running the latest version + * The `getPeers` endpoint will now report the calculated link cost for each given peer +* Upgrade dependencies + +### Fixed + +* Multicast discovery should now work again when building Yggdrasil as an Android framework +* Multicast discovery will now correctly ignore interfaces that are not marked as running +* Ephemeral links, such as those added by multicast, will no longer try to reconnect in a fast loop, fixing a high CPU issue +* The TUN interface will no longer stop working when hitting a segment read error from vectorised reads +* The `AllowedPublicKeys` option will once again no longer apply to multicast peerings, as was originally intended +* A potential panic when shutting down peering links has been fixed +* A redundant system call for setting MTU on OpenBSD has been removed + +## [0.5.8] - 2024-08-12 + +### Fixed + +* A bug which caused startup problems on Windows and FreeBSD should be fixed +* Resolved some minor link state and listener management bugs during shutdown + +## [0.5.7] - 2024-08-05 + +### Added + +* WebSocket support for peerings, by using the new `ws://` scheme in `Listen` and `Peers` + * Additionally, the `wss://` scheme can be used to connect to a WebSocket peer behind a HTTPS reverse proxy + +### Changed + +* On Linux, the TUN adapter now uses vectorised reads/writes where possible, which should reduce the amount of CPU time spent on syscalls and potentially improve throughput +* Link error handling has been improved and various link error messages have been rewritten to be clearer +* Upgrade dependencies + +### Fixed + +* Multiple multicast connections to the same remote machine should now work correctly + * You may get two connections in some cases, one inbound and one outbound, this is known and will not cause problems +* Running as a Windows service should be more reliable with service startup and shutdown bugs fixed + +## [0.5.6] - 2024-05-30 + +* Go 1.21 is now required to build Yggdrasil + +### Added + +* The `getPeers` endpoint now reports the RTT/latency of directly connected peers + +### Changed + +* The tree parent selection algorithm now prefers the lowest latency peers instead of the most stable +* Session key exchange logic has been changed to improve throughput and reduce occasional jitter + +### Fixed + +* Bloom filter hashing now works correctly on big-endian architectures +* Incorrect buffer pool usage has been fixed, reducing memory allocations +* The multicast beacon interval now backs off correctly, reducing the number of beacons sent +* A denial-of-service vulnerability in the QUIC library has been fixed with a dependency update + +## [0.5.5] - 2024-01-27 + +### Added + +* A new peer option `?maxbackoff=X` has been added to control the maximum backoff time for a given peer, supports duration values like `5m`, `1h` etc + +### Changed + +* The maximum backoff period for failing peer connections has been reduced to just over 1 hour, compared to 4.5 hours before +* The `getPeers` endpoint now sorts peers in a more stable fashion +* Upgrade dependencies + +### Fixed + +* A bug where QUIC listeners could stop listening for incoming connections unexpectedly has been fixed +* The priority tiebreak between multiple peerings to the same node has been fixed +* Peer connection ordering is no longer sensitive to poor system time resolution +* The admin socket now verifies the length of input public keys +* The `PPROFLISTEN` environment variable has been fixed and now starts the pprof listener correctly +* A panic in `getPeers` has been fixed when using abstract UNIX sockets on Linux + +## [0.5.4] - 2023-11-27 + +### Fixed + +* Fixed a crash that could happen when calculating the size of bloom filters during encoding + +## [0.5.3] - 2023-11-26 + +### Fixed + +* Fixed a data race from buffered pathfinder traffic +* Fix a bug where the next-hop selection may not take shortcuts through treespace +* Backoffs are now reset correctly when a successful handshake is completed +* Backoffs will no longer exceed roughly 4.5 hours when peers are down for a long time +* The `-normaliseconf` option will now work correctly with `PrivateKeyPath` +* Improved the reliability of QUIC peering setup by disabling 0-RTT + +## [0.5.2] - 2023-11-06 + +### Added + +* New `-publickey` command line option that prints the derived public key from a configuration file +* Support for connecting to TLS peers via SOCKS with the new `sockstls://` link schema + +### Changed + +* Stabilise tree parent selection algorithm +* Improved logging when the TUN interface fails to set up + +### Fixed + +* Fixed a panic that could occur when a connection reaches an inconsistent error state +* The admin socket will now report more peering handshake error conditions in `getPeers` +* Yggdrasil will no longer panic at startup when duplicate peers are configured +* The `build` script will no longer incorrectly import `LDFLAGS` from the environment + +## [0.5.1] - 2023-10-28 + +### Fixed + +* Fix the Debian package so that upgrades are handled more smoothly + +## [0.5.0] - 2023-10-28 + +### Added + +* Authenticated peering handshake with optional password, i.e. + * For listeners: `tls://[::]:12345?password=123456abcdef` + * For peers: `tls://a.b.c.d:12345?password=123456abcdef` + * For multicast interfaces with the new `Password` option in each `MulticastInterfaces` section + * Maximum password length is 64 characters +* QUIC support for peerings, by using the new `quic://` scheme in `Listen` and `Peers` + * This has not been extensively tested and may perform worse than TCP or TLS peers +* The private key can now be stored in PEM format separately to the main configuration file with the new `PrivateKeyPath` configuration file option + * Use the `-exportkey` flag to export the key to a file from an existing config + +### Changed + +* New routing scheme, which is backwards incompatible with previous versions of Yggdrasil + * The wire protocol version number, exchanged as part of the peer setup handshake, has been increased to 0.5 + * Nodes running this new version **will not** be able to peer with earlier versions of Yggdrasil + * A DHT is no longer used to map public keys and routes through treespace + * Bloom filters are used to track on-tree links and nodes reachable via that link + * Nodes now gossip separate per-link information which is tracked in CRDT structures, forcing local consistency and preventing unnecessary flapping when a route to the root node has changed or is broken + * Greedy routing is once again used instead of source routing + * Per-link keepalives have been replaced with periodic acknowledgements, reducing idle bandwidth +* The link handshake and multicast beacon formats have been revised for better future extensibility +* The link code has been refactored for more robust tracking of peering states + * As a result, the admin socket is now able to report information about configured peerings that are down + * Reconnect intervals are now tracked separately for each configured peer with exponential backoffs + +### Removed + +* Yggdrasil will no longer request BBR congestion control for TCP and TLS peerings on Linux + +## [0.4.7] - 2022-11-20 + +### Added + +* Dropped outbound peerings will now try to reconnect after a single second, rather than waiting up to 60 seconds for the normal peer timer + +### Changed + +* Session encryption keys are now rotated at most once per minute, which reduces CPU usage and improves throughput on fast low latency links +* Buffers are now reused in the session encryption handler, which improves session throughput and reduces memory allocations +* Buffers are now reused in the router for DHT and path traffic, which improves overall routing throughput and reduces memory allocations + +### Fixed + +* A bug in the admin socket where requests fail unless `arguments` is specified has been fixed +* Certificates on TLS listeners will no longer expire after a year +* The `-address` and `-subnet` command line options now return a useful warning when no configuration is specified + +## [0.4.6] - 2022-10-25 + +### Added + +* Support for prioritising multiple peerings to the same node has been added, useful for nodes with multiple network interfaces + * The priority can be configured by specifying `?priority=X` in a `Peers` or `Listen` URI, or by specifying `Priority` within a `MulticastInterfaces` configuration entry + * Priorities are values between 0 and 254 (default is 0), lower numbers are prioritised and nodes will automatically negotiate the higher of the two values + +### Changed + +* On Linux, `SO_REUSEADDR` is now used on the multicast port instead of `SO_REUSEPORT`, which should allow processes running under different users to run simultaneously + +### Fixed + +* Adding peers using the `InterfacePeers` configuration option should now work correctly again +* Multiple connections from the same remote IP address will no longer be incorrectly dropped +* The admin socket will no longer incorrectly claim TCP connections as TLS +* A panic that could occur when calling `GetPeers` while a peering link is being set up has been fixed + +## [0.4.5] - 2022-10-15 + +### Added + +* Support for peering over UNIX sockets is now available, by configuring `Listen` and peering URIs in the `unix:///path/to/socket.sock` format + +### Changed + +* `yggdrasilctl` has been refactored and now has cleaner output +* It is now possible to `addPeer` and `removePeer` using the admin socket again +* The `getSessions` admin socket call reports number of bytes received and transmitted again +* The link setup code has been refactored, making it easier to support new peering types in the future +* Yggdrasil now maintains configuration internally, rather than relying on a shared and potentially mutable structure + +### Fixed + +* Tracking information about expired root nodes has been fixed, which should hopefully resolve issues with reparenting and connection failures when the root node disappears +* A bug in the mobile framework code which caused a crash on Android when multicast failed to set up has been fixed +* Yggdrasil should now shut down gracefully and clean up correctly when running as a Windows service + ## [0.4.4] - 2022-07-07 ### Fixed -- ICMPv6 "Packet Too Big" payload size has been increased, which should fix Path MTU Discovery (PMTUD) when two nodes have different `IfMTU` values configured -- A crash has been fixed when handling debug packet responses -- `yggdrasilctl getSelf` should now report coordinates correctly again +* ICMPv6 "Packet Too Big" payload size has been increased, which should fix Path MTU Discovery (PMTUD) when two nodes have different `IfMTU` values configured +* A crash has been fixed when handling debug packet responses +* `yggdrasilctl getSelf` should now report coordinates correctly again ### Changed -- Go 1.17 is now required to build Yggdrasil +* Go 1.20 is now required to build Yggdrasil ## [0.4.3] - 2022-02-06 ### Added -- `bytes_sent`, `bytes_recvd` and `uptime` have been added to `getPeers` -- Clearer logging when connections are rejected due to incompatible peer versions +* `bytes_sent`, `bytes_recvd` and `uptime` have been added to `getPeers` +* Clearer logging when connections are rejected due to incompatible peer versions ### Fixed -- Latency-based parent selection tiebreak is now reliable on platforms even with low timer resolution -- Tree distance calculation offsets have been corrected +* Latency-based parent selection tiebreak is now reliable on platforms even with low timer resolution +* Tree distance calculation offsets have been corrected ## [0.4.2] - 2021-11-03 ### Fixed -- Reverted a dependency update which resulted in problems building with Go 1.16 and running on Windows +* Reverted a dependency update which resulted in problems building with Go 1.16 and running on Windows ## [0.4.1] - 2021-11-03 ### Added -- TLS peerings now support Server Name Indication (SNI) - - The SNI is sent automatically if the peering URI contains a DNS name - - A custom SNI can be specified by adding the `?sni=domain.com` parameter to the peering URI -- A new `ipv6rwc` API package now implements the IPv6-specific logic separate from the `tun` package +* TLS peerings now support Server Name Indication (SNI) + * The SNI is sent automatically if the peering URI contains a DNS name + * A custom SNI can be specified by adding the `?sni=domain.com` parameter to the peering URI +* A new `ipv6rwc` API package now implements the IPv6-specific logic separate from the `tun` package ### Fixed -- A crash when calculating the partial public key for very high IPv6 addresses has been fixed -- A crash due to a concurrent map write has been fixed -- A crash due to missing TUN configuration has been fixed -- A race condition in the keystore code has been fixed +* A crash when calculating the partial public key for very high IPv6 addresses has been fixed +* A crash due to a concurrent map write has been fixed +* A crash due to missing TUN configuration has been fixed +* A race condition in the keystore code has been fixed ## [0.4.0] - 2021-07-04 ### Added -- New routing scheme, which is backwards incompatible with previous versions of Yggdrasil - - The wire protocol version number, exchanged as part of the peer setup handshake, has been increased to 0.4 - - Nodes running this new version **will not** be able to peer with earlier versions of Yggdrasil - - Please note that **the network may be temporarily unstable** while infrastructure is being upgraded to the new release -- TLS connections now use public key pinning - - If no public key was already pinned, then the public key received as part of the TLS handshake is pinned to the connection - - The public key received as part of the handshake is checked against the pinned keys, and if no match is found, the connection is rejected +* New routing scheme, which is backwards incompatible with previous versions of Yggdrasil + * The wire protocol version number, exchanged as part of the peer setup handshake, has been increased to 0.4 + * Nodes running this new version **will not** be able to peer with earlier versions of Yggdrasil + * Please note that **the network may be temporarily unstable** while infrastructure is being upgraded to the new release +* TLS connections now use public key pinning + * If no public key was already pinned, then the public key received as part of the TLS handshake is pinned to the connection + * The public key received as part of the handshake is checked against the pinned keys, and if no match is found, the connection is rejected ### Changed -- IP addresses are now derived from ed25519 public (signing) keys - - Previously, addresses were derived from a hash of X25519 (Diffie-Hellman) keys - - Importantly, this means that **all internal IPv6 addresses will change with this release** — this will affect anyone running public services or relying on Yggdrasil for remote access -- It is now recommended to peer over TLS - - Link-local peers from multicast peer discovery will now connect over TLS, with the key from the multicast beacon pinned to the connection - - `socks://` peers now expect the destination endpoint to be a `tls://` listener, instead of a `tcp://` listener -- Multicast peer discovery is now more configurable - - There are separate configuration options to control if beacons are sent, what port to listen on for incoming connections (if sending beacons), and whether or not to listen for beacons from other nodes (and open connections when receiving a beacon) - - Each configuration entry in the list specifies a regular expression to match against interface names - - If an interface matches multiple regex in the list, it will use the settings for the first entry in the list that it matches with -- The session and routing code has been entirely redesigned and rewritten - - This is still an early work-in-progress, so the code hasn't been as well tested or optimized as the old code base — please bear with us for these next few releases as we work through any bugs or issues - - Generally speaking, we expect to see reduced bandwidth use and improved reliability with the new design, especially in cases where nodes move around or change peerings frequently - - Cryptographic sessions no longer use a single shared (ephemeral) secret for the entire life of the session. Keys are now rotated regularly for ongoing sessions (currently rotated at least once per round trip exchange of traffic, subject to change in future releases) - - Source routing has been added. Under normal circumstances, this is what is used to forward session traffic (e.g. the user's IPv6 traffic) - - DHT-based routing has been added. This is used when the sender does not know a source route to the destination. Forwarding through the DHT is less efficient, but the only information that it requires the sender to know is the destination node's (static) key. This is primarily used during the key exchange at session setup, or as a temporary fallback when a source route fails due to changes in the network - - The new DHT design is no longer RPC-based, does not support crawling and does not inherently allow nodes to look up the owner of an arbitrary key. Responding to lookups is now implemented at the application level and a response is only sent if the destination key matches the node's `/128` IP or `/64` prefix - - The greedy routing scheme, used to forward all traffic in previous releases, is now only used for protocol traffic (i.e. DHT setup and source route discovery) - - The routing logic now lives in a [standalone library](https://github.com/Arceliar/ironwood). You are encouraged **not** to use it, as it's still considered pre-alpha, but it's available for those who want to experiment with the new routing algorithm in other contexts - - Session MTUs may be slightly lower now, in order to accommodate large packet headers if required -- Many of the admin functions available over `yggdrasilctl` have been changed or removed as part of rewrites to the code - - Several remote `debug` functions have been added temporarily, to allow for crawling and census gathering during the transition to the new version, but we intend to remove this at some point in the (possibly distant) future - - The list of available functions will likely be expanded in future releases -- The configuration file format has been updated in response to the changed/removed features +* IP addresses are now derived from ed25519 public (signing) keys + * Previously, addresses were derived from a hash of X25519 (Diffie-Hellman) keys + * Importantly, this means that **all internal IPv6 addresses will change with this release** — this will affect anyone running public services or relying on Yggdrasil for remote access +* It is now recommended to peer over TLS + * Link-local peers from multicast peer discovery will now connect over TLS, with the key from the multicast beacon pinned to the connection + * `socks://` peers now expect the destination endpoint to be a `tls://` listener, instead of a `tcp://` listener +* Multicast peer discovery is now more configurable + * There are separate configuration options to control if beacons are sent, what port to listen on for incoming connections (if sending beacons), and whether or not to listen for beacons from other nodes (and open connections when receiving a beacon) + * Each configuration entry in the list specifies a regular expression to match against interface names + * If an interface matches multiple regex in the list, it will use the settings for the first entry in the list that it matches with +* The session and routing code has been entirely redesigned and rewritten + * This is still an early work-in-progress, so the code hasn't been as well tested or optimized as the old code base — please bear with us for these next few releases as we work through any bugs or issues + * Generally speaking, we expect to see reduced bandwidth use and improved reliability with the new design, especially in cases where nodes move around or change peerings frequently + * Cryptographic sessions no longer use a single shared (ephemeral) secret for the entire life of the session. Keys are now rotated regularly for ongoing sessions (currently rotated at least once per round trip exchange of traffic, subject to change in future releases) + * Source routing has been added. Under normal circumstances, this is what is used to forward session traffic (e.g. the user's IPv6 traffic) + * DHT-based routing has been added. This is used when the sender does not know a source route to the destination. Forwarding through the DHT is less efficient, but the only information that it requires the sender to know is the destination node's (static) key. This is primarily used during the key exchange at session setup, or as a temporary fallback when a source route fails due to changes in the network + * The new DHT design is no longer RPC-based, does not support crawling and does not inherently allow nodes to look up the owner of an arbitrary key. Responding to lookups is now implemented at the application level and a response is only sent if the destination key matches the node's `/128` IP or `/64` prefix + * The greedy routing scheme, used to forward all traffic in previous releases, is now only used for protocol traffic (i.e. DHT setup and source route discovery) + * The routing logic now lives in a [standalone library](https://github.com/Arceliar/ironwood). You are encouraged **not** to use it, as it's still considered pre-alpha, but it's available for those who want to experiment with the new routing algorithm in other contexts + * Session MTUs may be slightly lower now, in order to accommodate large packet headers if required +* Many of the admin functions available over `yggdrasilctl` have been changed or removed as part of rewrites to the code + * Several remote `debug` functions have been added temporarily, to allow for crawling and census gathering during the transition to the new version, but we intend to remove this at some point in the (possibly distant) future + * The list of available functions will likely be expanded in future releases +* The configuration file format has been updated in response to the changed/removed features ### Removed -- Tunnel routing (a.k.a. crypto-key routing or "CKR") has been removed - - It was far too easy to accidentally break routing altogether by capturing the route to peers with the TUN adapter - - We recommend tunnelling an existing standard over Yggdrasil instead (e.g. `ip6gre`, `ip6gretap` or other similar encapsulations, using Yggdrasil IPv6 addresses as the tunnel endpoints) - - All `TunnelRouting` configuration options will no longer take effect -- Session firewall has been removed - - This was never a true firewall — it didn't behave like a stateful IP firewall, often allowed return traffic unexpectedly and was simply a way to prevent a node from being flooded with unwanted sessions, so the name could be misleading and usually lead to a false sense of security - - Due to design changes, the new code needs to address the possible memory exhaustion attacks in other ways and a single configurable list no longer makes sense - - Users who want a firewall or other packet filter mechansim should configure something supported by their OS instead (e.g. `ip6tables`) - - All `SessionFirewall` configuration options will no longer take effect -- `SIGHUP` handling to reload the configuration at runtime has been removed - - It was not obvious which parts of the configuration could be reloaded at runtime, and which required the application to be killed and restarted to take effect - - Reloading the config without restarting was also a delicate and bug-prone process, and was distracting from more important developments - - `SIGHUP` will be handled normally (i.e. by exiting) -- `cmd/yggrasilsim` has been removed, and is unlikely to return to this repository +* Tunnel routing (a.k.a. crypto-key routing or "CKR") has been removed + * It was far too easy to accidentally break routing altogether by capturing the route to peers with the TUN adapter + * We recommend tunnelling an existing standard over Yggdrasil instead (e.g. `ip6gre`, `ip6gretap` or other similar encapsulations, using Yggdrasil IPv6 addresses as the tunnel endpoints) + * All `TunnelRouting` configuration options will no longer take effect +* Session firewall has been removed + * This was never a true firewall — it didn't behave like a stateful IP firewall, often allowed return traffic unexpectedly and was simply a way to prevent a node from being flooded with unwanted sessions, so the name could be misleading and usually lead to a false sense of security + * Due to design changes, the new code needs to address the possible memory exhaustion attacks in other ways and a single configurable list no longer makes sense + * Users who want a firewall or other packet filter mechansim should configure something supported by their OS instead (e.g. `ip6tables`) + * All `SessionFirewall` configuration options will no longer take effect +* `SIGHUP` handling to reload the configuration at runtime has been removed + * It was not obvious which parts of the configuration could be reloaded at runtime, and which required the application to be killed and restarted to take effect + * Reloading the config without restarting was also a delicate and bug-prone process, and was distracting from more important developments + * `SIGHUP` will be handled normally (i.e. by exiting) +* `cmd/yggrasilsim` has been removed, and is unlikely to return to this repository ## [0.3.16] - 2021-03-18 ### Added -- New simulation code under `cmd/yggdrasilsim` (work-in-progress) +* New simulation code under `cmd/yggdrasilsim` (work-in-progress) ### Changed -- Multi-threading in the switch - - Swich lookups happen independently for each (incoming) peer connection, instead of being funneled to a single dedicated switch worker - - Packets are queued for each (outgoing) peer connection, instead of being handled by a single dedicated switch worker -- Queue logic rewritten - - Heap structure per peer that traffic is routed to, with one FIFO queue per traffic flow - - The total size of each heap is configured automatically (we basically queue packets until we think we're blocked on a socket write) - - When adding to a full heap, the oldest packet from the largest queue is dropped - - Packets are popped from the queue in FIFO order (oldest packet from among all queues in the heap) to prevent packet reordering at the session level -- Removed global `sync.Pool` of `[]byte` - - Local `sync.Pool`s are used in the hot loops, but not exported, to avoid memory corruption if libraries are reused by other projects - - This may increase allocations (and slightly reduce speed in CPU-bound benchmarks) when interacting with the tun/tap device, but traffic forwarded at the switch layer should be unaffected -- Upgrade dependencies -- Upgrade build to Go 1.16 +* Multi-threading in the switch + * Swich lookups happen independently for each (incoming) peer connection, instead of being funneled to a single dedicated switch worker + * Packets are queued for each (outgoing) peer connection, instead of being handled by a single dedicated switch worker +* Queue logic rewritten + * Heap structure per peer that traffic is routed to, with one FIFO queue per traffic flow + * The total size of each heap is configured automatically (we basically queue packets until we think we're blocked on a socket write) + * When adding to a full heap, the oldest packet from the largest queue is dropped + * Packets are popped from the queue in FIFO order (oldest packet from among all queues in the heap) to prevent packet reordering at the session level +* Removed global `sync.Pool` of `[]byte` + * Local `sync.Pool`s are used in the hot loops, but not exported, to avoid memory corruption if libraries are reused by other projects + * This may increase allocations (and slightly reduce speed in CPU-bound benchmarks) when interacting with the tun/tap device, but traffic forwarded at the switch layer should be unaffected +* Upgrade dependencies +* Upgrade build to Go 1.16 ### Fixed -- Fixed a bug where the connection listener could exit prematurely due to resoruce exhaustion (if e.g. too many connections were opened) -- Fixed DefaultIfName for OpenBSD (`/dev/tun0` -> `tun0`) -- Fixed an issue where a peer could sometimes never be added to the switch -- Fixed a goroutine leak that could occur if a peer with an open connection continued to spam additional connection attempts +* Fixed a bug where the connection listener could exit prematurely due to resoruce exhaustion (if e.g. too many connections were opened) +* Fixed DefaultIfName for OpenBSD (`/dev/tun0` -> `tun0`) +* Fixed an issue where a peer could sometimes never be added to the switch +* Fixed a goroutine leak that could occur if a peer with an open connection continued to spam additional connection attempts ## [0.3.15] - 2020-09-27 ### Added -- Support for pinning remote public keys in peering strings has been added, e.g. - - By signing public key: `tcp://host:port?ed25519=key` - - By encryption public key: `tcp://host:port?curve25519=key` - - By both: `tcp://host:port?ed25519=key&curve25519=key` - - By multiple, in case of DNS round-robin or similar: `tcp://host:port?curve25519=key&curve25519=key&ed25519=key&ed25519=key` -- Some checks to prevent Yggdrasil-over-Yggdrasil peerings have been added -- Added support for SOCKS proxy authentication, e.g. `socks://user@password:host/...` +* Support for pinning remote public keys in peering strings has been added, e.g. + * By signing public key: `tcp://host:port?ed25519=key` + * By encryption public key: `tcp://host:port?curve25519=key` + * By both: `tcp://host:port?ed25519=key&curve25519=key` + * By multiple, in case of DNS round-robin or similar: `tcp://host:port?curve25519=key&curve25519=key&ed25519=key&ed25519=key` +* Some checks to prevent Yggdrasil-over-Yggdrasil peerings have been added +* Added support for SOCKS proxy authentication, e.g. `socks://user@password:host/...` ### Fixed -- Some bugs in the multicast code that could cause unnecessary CPU usage have been fixed -- A possible multicast deadlock on macOS when enumerating interfaces has been fixed -- A deadlock in the connection code has been fixed -- Updated HJSON dependency that caused some build problems +* Some bugs in the multicast code that could cause unnecessary CPU usage have been fixed +* A possible multicast deadlock on macOS when enumerating interfaces has been fixed +* A deadlock in the connection code has been fixed +* Updated HJSON dependency that caused some build problems ### Changed -- `DisconnectPeer` and `RemovePeer` have been separated and implemented properly now -- Less nodes are stored in the DHT now, reducing ambient network traffic and possible instability -- Default config file for FreeBSD is now at `/usr/local/etc/yggdrasil.conf` instead of `/etc/yggdrasil.conf` +* `DisconnectPeer` and `RemovePeer` have been separated and implemented properly now +* Less nodes are stored in the DHT now, reducing ambient network traffic and possible instability +* Default config file for FreeBSD is now at `/usr/local/etc/yggdrasil.conf` instead of `/etc/yggdrasil.conf` ## [0.3.14] - 2020-03-28 ### Fixed -- Fixes a memory leak that may occur if packets are incorrectly never removed from a switch queue +* Fixes a memory leak that may occur if packets are incorrectly never removed from a switch queue ### Changed -- Make DHT searches a bit more reliable by tracking the 16 most recently visited nodes +* Make DHT searches a bit more reliable by tracking the 16 most recently visited nodes ## [0.3.13] - 2020-02-21 ### Added -- Support for the Wireguard TUN driver, which now replaces Water and provides far better support and performance on Windows -- Windows `.msi` installer files are now supported (bundling the Wireguard TUN driver) -- NodeInfo code is now actorised, should be more reliable -- The DHT now tries to store the two closest nodes in either direction instead of one, such that if a node goes offline, the replacement is already known -- The Yggdrasil API now supports dialing a remote node using the public key instead of the Node ID +* Support for the Wireguard TUN driver, which now replaces Water and provides far better support and performance on Windows +* Windows `.msi` installer files are now supported (bundling the Wireguard TUN driver) +* NodeInfo code is now actorised, should be more reliable +* The DHT now tries to store the two closest nodes in either direction instead of one, such that if a node goes offline, the replacement is already known +* The Yggdrasil API now supports dialing a remote node using the public key instead of the Node ID ### Changed -- The `-loglevel` command line parameter is now cumulative and automatically includes all levels below the one specified -- DHT search code has been significantly simplified and processes rumoured nodes in parallel, speeding up search time -- DHT search results are now sorted -- The systemd service now handles configuration generation in a different unit -- The Yggdrasil API now returns public keys instead of node IDs when querying for local and remote addresses +* The `-loglevel` command line parameter is now cumulative and automatically includes all levels below the one specified +* DHT search code has been significantly simplified and processes rumoured nodes in parallel, speeding up search time +* DHT search results are now sorted +* The systemd service now handles configuration generation in a different unit +* The Yggdrasil API now returns public keys instead of node IDs when querying for local and remote addresses ### Fixed -- The multicast code no longer panics when shutting down the node -- A potential OOB error when calculating IPv4 flow labels (when tunnel routing is enabled) has been fixed -- A bug resulting in incorrect idle notifications in the switch should now be fixed -- MTUs are now using a common datatype throughout the codebase +* The multicast code no longer panics when shutting down the node +* A potential OOB error when calculating IPv4 flow labels (when tunnel routing is enabled) has been fixed +* A bug resulting in incorrect idle notifications in the switch should now be fixed +* MTUs are now using a common datatype throughout the codebase ### Removed -- TAP mode has been removed entirely, since it is no longer supported with the Wireguard TUN package. Please note that if you are using TAP mode, you may need to revise your config! -- NetBSD support has been removed until the Wireguard TUN package supports NetBSD +* TAP mode has been removed entirely, since it is no longer supported with the Wireguard TUN package. Please note that if you are using TAP mode, you may need to revise your config! +* NetBSD support has been removed until the Wireguard TUN package supports NetBSD ## [0.3.12] - 2019-11-24 ### Added -- New API functions `SetMaximumSessionMTU` and `GetMaximumSessionMTU` -- New command line parameters `-address` and `-subnet` for getting the address/subnet from the config file, for use with `-useconffile` or `-useconf` -- A warning is now produced in the Yggdrasil output at startup when the MTU in the config is invalid or has been adjusted for some reason +* New API functions `SetMaximumSessionMTU` and `GetMaximumSessionMTU` +* New command line parameters `-address` and `-subnet` for getting the address/subnet from the config file, for use with `-useconffile` or `-useconf` +* A warning is now produced in the Yggdrasil output at startup when the MTU in the config is invalid or has been adjusted for some reason ### Changed -- On Linux, outgoing `InterfacePeers` connections now use `SO_BINDTODEVICE` to prefer an outgoing interface -- The `genkeys` utility is now in `cmd` rather than `misc` +* On Linux, outgoing `InterfacePeers` connections now use `SO_BINDTODEVICE` to prefer an outgoing interface +* The `genkeys` utility is now in `cmd` rather than `misc` ### Fixed -- A data race condition has been fixed when updating session coordinates -- A crash when shutting down when no multicast interfaces are configured has been fixed -- A deadlock when calling `AddPeer` multiple times has been fixed -- A typo in the systemd unit file (for some Linux packages) has been fixed -- The NodeInfo and admin socket now report `unknown` correctly when no build name/version is available in the environment at build time -- The MTU calculation now correctly accounts for ethernet headers when running in TAP mode +* A data race condition has been fixed when updating session coordinates +* A crash when shutting down when no multicast interfaces are configured has been fixed +* A deadlock when calling `AddPeer` multiple times has been fixed +* A typo in the systemd unit file (for some Linux packages) has been fixed +* The NodeInfo and admin socket now report `unknown` correctly when no build name/version is available in the environment at build time +* The MTU calculation now correctly accounts for ethernet headers when running in TAP mode ## [0.3.11] - 2019-10-25 ### Added -- Support for TLS listeners and peers has been added, allowing the use of `tls://host:port` in `Peers`, `InterfacePeers` and `Listen` configuration settings - this allows hiding Yggdrasil peerings inside regular TLS connections +* Support for TLS listeners and peers has been added, allowing the use of `tls://host:port` in `Peers`, `InterfacePeers` and `Listen` configuration settings - this allows hiding Yggdrasil peerings inside regular TLS connections ### Changed -- Go 1.13 or later is now required for building Yggdrasil -- Some exported API functions have been updated to work with standard Go interfaces: - - `net.Conn` instead of `yggdrasil.Conn` - - `net.Dialer` (the interface it would satisfy if it wasn't a concrete type) instead of `yggdrasil.Dialer` - - `net.Listener` instead of `yggdrasil.Listener` -- Session metadata is now updated correctly when a search completes for a node to which we already have an open session -- Multicast module reloading behaviour has been improved +* Go 1.13 or later is now required for building Yggdrasil +* Some exported API functions have been updated to work with standard Go interfaces: + * `net.Conn` instead of `yggdrasil.Conn` + * `net.Dialer` (the interface it would satisfy if it wasn't a concrete type) instead of `yggdrasil.Dialer` + * `net.Listener` instead of `yggdrasil.Listener` +* Session metadata is now updated correctly when a search completes for a node to which we already have an open session +* Multicast module reloading behaviour has been improved ### Fixed -- An incorrectly held mutex in the crypto-key routing code has been fixed -- Multicast module no longer opens a listener socket if no multicast interfaces are configured +* An incorrectly held mutex in the crypto-key routing code has been fixed +* Multicast module no longer opens a listener socket if no multicast interfaces are configured ## [0.3.10] - 2019-10-10 ### Added -- The core library now includes several unit tests for peering and `yggdrasil.Conn` connections +* The core library now includes several unit tests for peering and `yggdrasil.Conn` connections ### Changed -- On recent Linux kernels, Yggdrasil will now set the `tcp_congestion_control` algorithm used for its own TCP sockets to [BBR](https://github.com/google/bbr), which reduces latency under load -- The systemd service configuration in `contrib` (and, by extension, some of our packages) now attempts to load the `tun` module, in case TUN/TAP support is available but not loaded, and it restricts Yggdrasil to the `CAP_NET_ADMIN` capability for managing the TUN/TAP adapter, rather than letting it do whatever the (typically `root`) user can do +* On recent Linux kernels, Yggdrasil will now set the `tcp_congestion_control` algorithm used for its own TCP sockets to [BBR](https://github.com/google/bbr), which reduces latency under load +* The systemd service configuration in `contrib` (and, by extension, some of our packages) now attempts to load the `tun` module, in case TUN/TAP support is available but not loaded, and it restricts Yggdrasil to the `CAP_NET_ADMIN` capability for managing the TUN/TAP adapter, rather than letting it do whatever the (typically `root`) user can do ### Fixed -- The `yggdrasil.Conn.RemoteAddr()` function no longer blocks, fixing a deadlock when CKR is used while under heavy load +* The `yggdrasil.Conn.RemoteAddr()` function no longer blocks, fixing a deadlock when CKR is used while under heavy load ## [0.3.9] - 2019-09-27 ### Added -- Yggdrasil will now complain more verbosely when a peer URI is incorrectly formatted -- Soft-shutdown methods have been added, allowing a node to shut down gracefully when terminated -- New multicast interval logic which sends multicast beacons more often when Yggdrasil is first started to increase the chance of finding nearby nodes quickly after startup +* Yggdrasil will now complain more verbosely when a peer URI is incorrectly formatted +* Soft-shutdown methods have been added, allowing a node to shut down gracefully when terminated +* New multicast interval logic which sends multicast beacons more often when Yggdrasil is first started to increase the chance of finding nearby nodes quickly after startup ### Changed -- The switch now buffers packets more eagerly in an attempt to give the best link a chance to send, which appears to reduce packet reordering when crossing aggregate sets of peerings -- Substantial amounts of the codebase have been refactored to use the actor model, which should substantially reduce the chance of deadlocks -- Nonce tracking in sessions has been modified so that memory usage is reduced whilst still only allowing duplicate packets within a small window -- Soft-reconfiguration support has been simplified using new actor functions -- The garbage collector threshold has been adjusted for mobile builds -- The maximum queue size is now managed exclusively by the switch rather than by the core +* The switch now buffers packets more eagerly in an attempt to give the best link a chance to send, which appears to reduce packet reordering when crossing aggregate sets of peerings +* Substantial amounts of the codebase have been refactored to use the actor model, which should substantially reduce the chance of deadlocks +* Nonce tracking in sessions has been modified so that memory usage is reduced whilst still only allowing duplicate packets within a small window +* Soft-reconfiguration support has been simplified using new actor functions +* The garbage collector threshold has been adjusted for mobile builds +* The maximum queue size is now managed exclusively by the switch rather than by the core ### Fixed -- The broken `hjson-go` dependency which affected builds of the previous version has now been resolved in the module manifest -- Some minor memory leaks in the switch have been fixed, which improves memory usage on mobile builds -- A memory leak in the add-peer loop has been fixed -- The admin socket now reports the correct URI strings for SOCKS peers in `getPeers` -- A race condition when dialing a remote node by both the node address and routed prefix simultaneously has been fixed -- A race condition between the router and the dial code resulting in a panic has been fixed -- A panic which could occur when the TUN/TAP interface disappears (e.g. during soft-shutdown) has been fixed -- A bug in the semantic versioning script which accompanies Yggdrasil for builds has been fixed -- A panic which could occur when the TUN/TAP interface reads an undersized/corrupted packet has been fixed +* The broken `hjson-go` dependency which affected builds of the previous version has now been resolved in the module manifest +* Some minor memory leaks in the switch have been fixed, which improves memory usage on mobile builds +* A memory leak in the add-peer loop has been fixed +* The admin socket now reports the correct URI strings for SOCKS peers in `getPeers` +* A race condition when dialing a remote node by both the node address and routed prefix simultaneously has been fixed +* A race condition between the router and the dial code resulting in a panic has been fixed +* A panic which could occur when the TUN/TAP interface disappears (e.g. during soft-shutdown) has been fixed +* A bug in the semantic versioning script which accompanies Yggdrasil for builds has been fixed +* A panic which could occur when the TUN/TAP interface reads an undersized/corrupted packet has been fixed ### Removed -- A number of legacy debug functions have now been removed and a number of exported API functions are now better documented +* A number of legacy debug functions have now been removed and a number of exported API functions are now better documented ## [0.3.8] - 2019-08-21 ### Changed -- Yggdrasil can now send multiple packets from the switch at once, which results in improved throughput with smaller packets or lower MTUs -- Performance has been slightly improved by not allocating cancellations where not necessary -- Crypto-key routing options have been renamed for clarity - - `IPv4Sources` is now named `IPv4LocalSubnets` - - `IPv6Sources` is now named `IPv6LocalSubnets` - - `IPv4Destinations` is now named `IPv4RemoteSubnets` - - `IPv6Destinations` is now named `IPv6RemoteSubnets` - - The old option names will continue to be accepted by the configuration parser for now but may not be indefinitely -- When presented with multiple paths between two nodes, the switch now prefers the most recently used port when possible instead of the least recently used, helping to reduce packet reordering -- New nonce tracking should help to reduce the number of packets dropped as a result of multiple/aggregate paths or congestion control in the switch +* Yggdrasil can now send multiple packets from the switch at once, which results in improved throughput with smaller packets or lower MTUs +* Performance has been slightly improved by not allocating cancellations where not necessary +* Crypto-key routing options have been renamed for clarity + * `IPv4Sources` is now named `IPv4LocalSubnets` + * `IPv6Sources` is now named `IPv6LocalSubnets` + * `IPv4Destinations` is now named `IPv4RemoteSubnets` + * `IPv6Destinations` is now named `IPv6RemoteSubnets` + * The old option names will continue to be accepted by the configuration parser for now but may not be indefinitely +* When presented with multiple paths between two nodes, the switch now prefers the most recently used port when possible instead of the least recently used, helping to reduce packet reordering +* New nonce tracking should help to reduce the number of packets dropped as a result of multiple/aggregate paths or congestion control in the switch ### Fixed -- A deadlock was fixed in the session code which could result in Yggdrasil failing to pass traffic after some time +* A deadlock was fixed in the session code which could result in Yggdrasil failing to pass traffic after some time ### Security -- Address verification was not strict enough, which could result in a malicious session sending traffic with unexpected or spoofed source or destination addresses which Yggdrasil could fail to reject - - Versions `0.3.6` and `0.3.7` are vulnerable - users of these versions should upgrade as soon as possible - - Versions `0.3.5` and earlier are not affected +* Address verification was not strict enough, which could result in a malicious session sending traffic with unexpected or spoofed source or destination addresses which Yggdrasil could fail to reject + * Versions `0.3.6` and `0.3.7` are vulnerable - users of these versions should upgrade as soon as possible + * Versions `0.3.5` and earlier are not affected ## [0.3.7] - 2019-08-14 ### Changed -- The switch should now forward packets along a single path more consistently in cases where congestion is low and multiple equal-length paths exist, which should improve stability and result in fewer out-of-order packets -- Sessions should now be more tolerant of out-of-order packets, by replacing a bitmask with a variable sized heap+map structure to track recently received nonces, which should reduce the number of packets dropped due to reordering when multiple paths are used or multiple independent flows are transmitted through the same session -- The admin socket can no longer return a dotfile representation of the known parts of the network, this could be rebuilt by clients using information from `getSwitchPeers`,`getDHT` and `getSessions` +* The switch should now forward packets along a single path more consistently in cases where congestion is low and multiple equal-length paths exist, which should improve stability and result in fewer out-of-order packets +* Sessions should now be more tolerant of out-of-order packets, by replacing a bitmask with a variable sized heap+map structure to track recently received nonces, which should reduce the number of packets dropped due to reordering when multiple paths are used or multiple independent flows are transmitted through the same session +* The admin socket can no longer return a dotfile representation of the known parts of the network, this could be rebuilt by clients using information from `getSwitchPeers`,`getDHT` and `getSessions` ### Fixed -- A number of significant performance regressions introduced in version 0.3.6 have been fixed, resulting in better performance -- Flow labels are now used to prioritise traffic flows again correctly -- In low-traffic scenarios where there are multiple peerings between a pair of nodes, Yggdrasil now prefers the most active peering instead of the least active, helping to reduce packet reordering -- The `Listen` statement, when configured as a string rather than an array, will now be parsed correctly -- The admin socket now returns `coords` as a correct array of unsigned 64-bit integers, rather than the internal representation -- The admin socket now returns `box_pub_key` in string format again -- Sessions no longer leak/block when no listener (e.g. TUN/TAP) is configured -- Incoming session connections no longer block when a session already exists, which results in less leaked goroutines -- Flooded sessions will no longer block other sessions -- Searches are now cleaned up properly and a couple of edge-cases with duplicate searches have been fixed -- A number of minor allocation and pointer fixes +* A number of significant performance regressions introduced in version 0.3.6 have been fixed, resulting in better performance +* Flow labels are now used to prioritise traffic flows again correctly +* In low-traffic scenarios where there are multiple peerings between a pair of nodes, Yggdrasil now prefers the most active peering instead of the least active, helping to reduce packet reordering +* The `Listen` statement, when configured as a string rather than an array, will now be parsed correctly +* The admin socket now returns `coords` as a correct array of unsigned 64-bit integers, rather than the internal representation +* The admin socket now returns `box_pub_key` in string format again +* Sessions no longer leak/block when no listener (e.g. TUN/TAP) is configured +* Incoming session connections no longer block when a session already exists, which results in less leaked goroutines +* Flooded sessions will no longer block other sessions +* Searches are now cleaned up properly and a couple of edge-cases with duplicate searches have been fixed +* A number of minor allocation and pointer fixes ## [0.3.6] - 2019-08-03 ### Added -- Yggdrasil now has a public API with interfaces such as `yggdrasil.ConnDialer`, `yggdrasil.ConnListener` and `yggdrasil.Conn` for using Yggdrasil as a transport directly within applications -- Session gatekeeper functions, part of the API, which can be used to control whether to allow or reject incoming or outgoing sessions dynamically (compared to the previous fixed whitelist/blacklist approach) -- Support for logging to files or syslog (where supported) -- Platform defaults now include the ability to set sane defaults for multicast interfaces +* Yggdrasil now has a public API with interfaces such as `yggdrasil.ConnDialer`, `yggdrasil.ConnListener` and `yggdrasil.Conn` for using Yggdrasil as a transport directly within applications +* Session gatekeeper functions, part of the API, which can be used to control whether to allow or reject incoming or outgoing sessions dynamically (compared to the previous fixed whitelist/blacklist approach) +* Support for logging to files or syslog (where supported) +* Platform defaults now include the ability to set sane defaults for multicast interfaces ### Changed -- Following a massive refactoring exercise, Yggdrasil's codebase has now been broken out into modules -- Core node functionality in the `yggdrasil` package with a public API - - This allows Yggdrasil to be integrated directly into other applications and used as a transport - - IP-specific code has now been moved out of the core `yggdrasil` package, making Yggdrasil effectively protocol-agnostic -- Multicast peer discovery functionality is now in the `multicast` package -- Admin socket functionality is now in the `admin` package and uses the Yggdrasil public API -- TUN/TAP, ICMPv6 and all IP-specific functionality is now in the `tuntap` package -- `PPROF` debug output is now sent to `stderr` instead of `stdout` -- Node IPv6 addresses on macOS are now configured as `secured` -- Upstream dependency references have been updated, which includes a number of fixes in the Water library +* Following a massive refactoring exercise, Yggdrasil's codebase has now been broken out into modules +* Core node functionality in the `yggdrasil` package with a public API + * This allows Yggdrasil to be integrated directly into other applications and used as a transport + * IP-specific code has now been moved out of the core `yggdrasil` package, making Yggdrasil effectively protocol-agnostic +* Multicast peer discovery functionality is now in the `multicast` package +* Admin socket functionality is now in the `admin` package and uses the Yggdrasil public API +* TUN/TAP, ICMPv6 and all IP-specific functionality is now in the `tuntap` package +* `PPROF` debug output is now sent to `stderr` instead of `stdout` +* Node IPv6 addresses on macOS are now configured as `secured` +* Upstream dependency references have been updated, which includes a number of fixes in the Water library ### Fixed -- Multicast discovery is no longer disabled if the nominated interfaces aren't available on the system yet, e.g. during boot -- Multicast interfaces are now re-evaluated more frequently so that Yggdrasil doesn't need to be restarted to use interfaces that have become available since startup -- Admin socket error cases are now handled better -- Various fixes in the TUN/TAP module, particularly surrounding Windows platform support -- Invalid keys will now cause the node to fail to start, rather than starting but silently not working as before -- Session MTUs are now always calculated correctly, in some cases they were incorrectly defaulting to 1280 before -- Multiple searches now don't take place for a single connection -- Concurrency bugs fixed -- Fixed a number of bugs in the ICMPv6 neighbor solicitation in the TUN/TAP code -- A case where peers weren't always added correctly if one or more peers were unreachable has been fixed -- Searches which include the local node are now handled correctly -- Lots of small bug tweaks and clean-ups throughout the codebase +* Multicast discovery is no longer disabled if the nominated interfaces aren't available on the system yet, e.g. during boot +* Multicast interfaces are now re-evaluated more frequently so that Yggdrasil doesn't need to be restarted to use interfaces that have become available since startup +* Admin socket error cases are now handled better +* Various fixes in the TUN/TAP module, particularly surrounding Windows platform support +* Invalid keys will now cause the node to fail to start, rather than starting but silently not working as before +* Session MTUs are now always calculated correctly, in some cases they were incorrectly defaulting to 1280 before +* Multiple searches now don't take place for a single connection +* Concurrency bugs fixed +* Fixed a number of bugs in the ICMPv6 neighbor solicitation in the TUN/TAP code +* A case where peers weren't always added correctly if one or more peers were unreachable has been fixed +* Searches which include the local node are now handled correctly +* Lots of small bug tweaks and clean-ups throughout the codebase ## [0.3.5] - 2019-03-13 ### Fixed -- The `AllowedEncryptionPublicKeys` option has now been fixed to handle incoming connections properly and no longer blocks outgoing connections (this was broken in v0.3.4) -- Multicast TCP listeners will now be stopped correctly when the link-local address on the interface changes or disappears altogether +* The `AllowedEncryptionPublicKeys` option has now been fixed to handle incoming connections properly and no longer blocks outgoing connections (this was broken in v0.3.4) +* Multicast TCP listeners will now be stopped correctly when the link-local address on the interface changes or disappears altogether ## [0.3.4] - 2019-03-12 ### Added -- Support for multiple listeners (although currently only TCP listeners are supported) -- New multicast behaviour where each multicast interface is given its own link-local listener and does not depend on the `Listen` configuration -- Blocking detection in the switch to avoid parenting a blocked peer -- Support for adding and removing listeners and multicast interfaces when reloading configuration during runtime -- Yggdrasil will now attempt to clean up UNIX admin sockets on startup if left behind by a previous crash -- Admin socket `getTunnelRouting` and `setTunnelRouting` calls for enabling and disabling crypto-key routing during runtime -- On macOS, Yggdrasil will now try to wake up AWDL on start-up when `awdl0` is a configured multicast interface, to keep it awake after system sleep, and to stop waking it when no longer needed -- Added `LinkLocalTCPPort` option for controlling the port number that link-local TCP listeners will listen on by default when setting up `MulticastInterfaces` (a node restart is currently required for changes to `LinkLocalTCPPort` to take effect - it cannot be updated by reloading config during runtime) +* Support for multiple listeners (although currently only TCP listeners are supported) +* New multicast behaviour where each multicast interface is given its own link-local listener and does not depend on the `Listen` configuration +* Blocking detection in the switch to avoid parenting a blocked peer +* Support for adding and removing listeners and multicast interfaces when reloading configuration during runtime +* Yggdrasil will now attempt to clean up UNIX admin sockets on startup if left behind by a previous crash +* Admin socket `getTunnelRouting` and `setTunnelRouting` calls for enabling and disabling crypto-key routing during runtime +* On macOS, Yggdrasil will now try to wake up AWDL on start-up when `awdl0` is a configured multicast interface, to keep it awake after system sleep, and to stop waking it when no longer needed +* Added `LinkLocalTCPPort` option for controlling the port number that link-local TCP listeners will listen on by default when setting up `MulticastInterfaces` (a node restart is currently required for changes to `LinkLocalTCPPort` to take effect - it cannot be updated by reloading config during runtime) ### Changed -- The `Listen` configuration statement is now an array instead of a string -- The `Listen` configuration statement should now conform to the same formatting as peers with the protocol prefix, e.g. `tcp://[::]:0` -- Session workers are now non-blocking -- Multicast interval is now fixed at every 15 seconds and network interfaces are reevaluated for eligibility on each interval (where before the interval depended upon the number of configured multicast interfaces and evaluation only took place at startup) -- Dead connections are now closed in the link handler as opposed to the switch -- Peer forwarding is now prioritised instead of randomised +* The `Listen` configuration statement is now an array instead of a string +* The `Listen` configuration statement should now conform to the same formatting as peers with the protocol prefix, e.g. `tcp://[::]:0` +* Session workers are now non-blocking +* Multicast interval is now fixed at every 15 seconds and network interfaces are reevaluated for eligibility on each interval (where before the interval depended upon the number of configured multicast interfaces and evaluation only took place at startup) +* Dead connections are now closed in the link handler as opposed to the switch +* Peer forwarding is now prioritised instead of randomised ### Fixed -- Admin socket `getTunTap` call now returns properly instead of claiming no interface is enabled in all cases -- Handling of `getRoutes` etc in `yggdrasilctl` is now working -- Local interface names are no longer leaked in multicast packets -- Link-local TCP connections, particularly those initiated because of multicast beacons, are now always correctly scoped for the target interface -- Yggdrasil now correctly responds to multicast interfaces going up and down during runtime +* Admin socket `getTunTap` call now returns properly instead of claiming no interface is enabled in all cases +* Handling of `getRoutes` etc in `yggdrasilctl` is now working +* Local interface names are no longer leaked in multicast packets +* Link-local TCP connections, particularly those initiated because of multicast beacons, are now always correctly scoped for the target interface +* Yggdrasil now correctly responds to multicast interfaces going up and down during runtime ## [0.3.3] - 2019-02-18 ### Added -- Dynamic reconfiguration, which allows reloading the configuration file to make changes during runtime by sending a `SIGHUP` signal (note: this only works with `-useconffile` and not `-useconf` and currently reconfiguring TUN/TAP is not supported) -- Support for building Yggdrasil as an iOS or Android framework if the appropriate tools (e.g. `gomobile`/`gobind` + SDKs) are available -- Connection contexts used for TCP connections which allow more exotic socket options to be set, e.g. - - Reusing the multicast socket to allow multiple running Yggdrasil instances without having to disable multicast - - Allowing supported Macs to peer with other nearby Macs that aren't even on the same Wi-Fi network using AWDL -- Flexible logging support, which allows for logging at different levels of verbosity +* Dynamic reconfiguration, which allows reloading the configuration file to make changes during runtime by sending a `SIGHUP` signal (note: this only works with `-useconffile` and not `-useconf` and currently reconfiguring TUN/TAP is not supported) +* Support for building Yggdrasil as an iOS or Android framework if the appropriate tools (e.g. `gomobile`/`gobind` + SDKs) are available +* Connection contexts used for TCP connections which allow more exotic socket options to be set, e.g. + * Reusing the multicast socket to allow multiple running Yggdrasil instances without having to disable multicast + * Allowing supported Macs to peer with other nearby Macs that aren't even on the same Wi-Fi network using AWDL +* Flexible logging support, which allows for logging at different levels of verbosity ### Changed -- Switch changes to improve parent selection -- Node configuration is now stored centrally, rather than having fragments/copies distributed at startup time -- Significant refactoring in various areas, including for link types (TCP, AWDL etc), generic streams and adapters -- macOS builds through CircleCI are now 64-bit only +* Switch changes to improve parent selection +* Node configuration is now stored centrally, rather than having fragments/copies distributed at startup time +* Significant refactoring in various areas, including for link types (TCP, AWDL etc), generic streams and adapters +* macOS builds through CircleCI are now 64-bit only ### Fixed -- Simplified `systemd` service now in `contrib` +* Simplified `systemd` service now in `contrib` ### Removed -- `ReadTimeout` option is now deprecated +* `ReadTimeout` option is now deprecated ## [0.3.2] - 2018-12-26 ### Added -- The admin socket is now multithreaded, greatly improving performance of the crawler and allowing concurrent lookups to take place -- The ability to hide NodeInfo defaults through either setting the `NodeInfoPrivacy` option or through setting individual `NodeInfo` attributes to `null` +* The admin socket is now multithreaded, greatly improving performance of the crawler and allowing concurrent lookups to take place +* The ability to hide NodeInfo defaults through either setting the `NodeInfoPrivacy` option or through setting individual `NodeInfo` attributes to `null` ### Changed -- The `armhf` build now targets ARMv6 instead of ARMv7, adding support for Raspberry Pi Zero and other older models, amongst others +* The `armhf` build now targets ARMv6 instead of ARMv7, adding support for Raspberry Pi Zero and other older models, amongst others ### Fixed -- DHT entries are now populated using a copy in memory to fix various potential DHT bugs -- DHT traffic should now throttle back exponentially to reduce idle traffic -- Adjust how nodes are inserted into the DHT which should help to reduce some incorrect DHT traffic -- In TAP mode, the NDP target address is now correctly used when populating the peer MAC table. This fixes serious connectivity problems when in TAP mode, particularly on BSD -- In TUN mode, ICMPv6 packets are now ignored whereas they were incorrectly processed before +* DHT entries are now populated using a copy in memory to fix various potential DHT bugs +* DHT traffic should now throttle back exponentially to reduce idle traffic +* Adjust how nodes are inserted into the DHT which should help to reduce some incorrect DHT traffic +* In TAP mode, the NDP target address is now correctly used when populating the peer MAC table. This fixes serious connectivity problems when in TAP mode, particularly on BSD +* In TUN mode, ICMPv6 packets are now ignored whereas they were incorrectly processed before ## [0.3.1] - 2018-12-17 ### Added -- Build name and version is now imprinted onto the binaries if available/specified during build -- Ability to disable admin socket with `AdminListen: none` -- `AF_UNIX` domain sockets for the admin socket -- Cache size restriction for crypto-key routes -- `NodeInfo` support for specifying node information, e.g. node name or contact, which can be used in network crawls or surveys -- `getNodeInfo` request added to admin socket -- Adds flags `-c`, `-l` and `-t` to `build` script for specifying `GCFLAGS`, `LDFLAGS` or whether to keep symbol/DWARF tables +* Build name and version is now imprinted onto the binaries if available/specified during build +* Ability to disable admin socket with `AdminListen: none` +* `AF_UNIX` domain sockets for the admin socket +* Cache size restriction for crypto-key routes +* `NodeInfo` support for specifying node information, e.g. node name or contact, which can be used in network crawls or surveys +* `getNodeInfo` request added to admin socket +* Adds flags `-c`, `-l` and `-t` to `build` script for specifying `GCFLAGS`, `LDFLAGS` or whether to keep symbol/DWARF tables ### Changed -- Default `AdminListen` in newly generated config is now `unix:///var/run/yggdrasil.sock` -- Formatting of `getRoutes` in the admin socket has been improved -- Debian package now adds `yggdrasil` group to assist with `AF_UNIX` admin socket permissions -- Crypto, address and other utility code refactored into separate Go packages +* Default `AdminListen` in newly generated config is now `unix:///var/run/yggdrasil.sock` +* Formatting of `getRoutes` in the admin socket has been improved +* Debian package now adds `yggdrasil` group to assist with `AF_UNIX` admin socket permissions +* Crypto, address and other utility code refactored into separate Go packages ### Fixed -- Switch peer convergence is now much faster again (previously it was taking up to a minute once the peering was established) -- `yggdrasilctl` is now less prone to crashing when parameters are specified incorrectly -- Panic fixed when `Peers` or `InterfacePeers` was commented out +* Switch peer convergence is now much faster again (previously it was taking up to a minute once the peering was established) +* `yggdrasilctl` is now less prone to crashing when parameters are specified incorrectly +* Panic fixed when `Peers` or `InterfacePeers` was commented out ## [0.3.0] - 2018-12-12 ### Added -- Crypto-key routing support for tunnelling both IPv4 and IPv6 over Yggdrasil -- Add advanced `SwitchOptions` in configuration file for tuning the switch -- Add `dhtPing` to the admin socket to aid in crawling the network -- New macOS .pkgs built automatically by CircleCI -- Add Dockerfile to repository for Docker support -- Add `-json` command line flag for generating and normalising configuration in plain JSON instead of HJSON -- Build name and version numbers are now imprinted onto the build, accessible through `yggdrasil -version` and `yggdrasilctl getSelf` -- Add ability to disable admin socket by setting `AdminListen` to `"none"` -- `yggdrasilctl` now tries to look for the default configuration file to find `AdminListen` if `-endpoint` is not specified -- `yggdrasilctl` now returns more useful logging in the event of a fatal error +* Crypto-key routing support for tunnelling both IPv4 and IPv6 over Yggdrasil +* Add advanced `SwitchOptions` in configuration file for tuning the switch +* Add `dhtPing` to the admin socket to aid in crawling the network +* New macOS .pkgs built automatically by CircleCI +* Add Dockerfile to repository for Docker support +* Add `-json` command line flag for generating and normalising configuration in plain JSON instead of HJSON +* Build name and version numbers are now imprinted onto the build, accessible through `yggdrasil -version` and `yggdrasilctl getSelf` +* Add ability to disable admin socket by setting `AdminListen` to `"none"` +* `yggdrasilctl` now tries to look for the default configuration file to find `AdminListen` if `-endpoint` is not specified +* `yggdrasilctl` now returns more useful logging in the event of a fatal error ### Changed -- Switched to Chord DHT (instead of Kademlia, although still compatible at the protocol level) -- The `AdminListen` option and `yggdrasilctl` now default to `unix:///var/run/yggdrasil.sock` on BSDs, macOS and Linux -- Cleaned up some of the parameter naming in the admin socket -- Latency-based parent selection for the switch instead of uptime-based (should help to avoid high latency links somewhat) -- Real peering endpoints now shown in the admin socket `getPeers` call to help identify peerings -- Reuse the multicast port on supported platforms so that multiple Yggdrasil processes can run -- `yggdrasilctl` now has more useful help text (with `-help` or when no arguments passed) +* Switched to Chord DHT (instead of Kademlia, although still compatible at the protocol level) +* The `AdminListen` option and `yggdrasilctl` now default to `unix:///var/run/yggdrasil.sock` on BSDs, macOS and Linux +* Cleaned up some of the parameter naming in the admin socket +* Latency-based parent selection for the switch instead of uptime-based (should help to avoid high latency links somewhat) +* Real peering endpoints now shown in the admin socket `getPeers` call to help identify peerings +* Reuse the multicast port on supported platforms so that multiple Yggdrasil processes can run +* `yggdrasilctl` now has more useful help text (with `-help` or when no arguments passed) ### Fixed -- Memory leaks in the DHT fixed -- Crash fixed where the ICMPv6 NDP goroutine would incorrectly start in TUN mode -- Removing peers from the switch table if they stop sending switch messages but keep the TCP connection alive +* Memory leaks in the DHT fixed +* Crash fixed where the ICMPv6 NDP goroutine would incorrectly start in TUN mode +* Removing peers from the switch table if they stop sending switch messages but keep the TCP connection alive ## [0.2.7] - 2018-10-13 ### Added -- Session firewall, which makes it possible to control who can open sessions with your node -- Add `getSwitchQueues` to admin socket -- Add `InterfacePeers` for configuring static peerings via specific network interfaces -- More output shown in `getSwitchPeers` -- FreeBSD service script in `contrib` +* Session firewall, which makes it possible to control who can open sessions with your node +* Add `getSwitchQueues` to admin socket +* Add `InterfacePeers` for configuring static peerings via specific network interfaces +* More output shown in `getSwitchPeers` +* FreeBSD service script in `contrib` ## Changed -- CircleCI builds are now built with Go 1.11 instead of Go 1.9 +* CircleCI builds are now built with Go 1.11 instead of Go 1.9 ## Fixed -- Race condition in the switch table, reported by trn -- Debug builds are now tested by CircleCI as well as platform release builds -- Port number fixed on admin graph from unknown nodes +* Race condition in the switch table, reported by trn +* Debug builds are now tested by CircleCI as well as platform release builds +* Port number fixed on admin graph from unknown nodes ## [0.2.6] - 2018-07-31 ### Added -- Configurable TCP timeouts to assist in peering over Tor/I2P -- Prefer IPv6 flow label when extending coordinates to sort backpressure queues -- `arm64` builds through CircleCI +* Configurable TCP timeouts to assist in peering over Tor/I2P +* Prefer IPv6 flow label when extending coordinates to sort backpressure queues +* `arm64` builds through CircleCI ### Changed -- Sort dot graph links by integer value +* Sort dot graph links by integer value ## [0.2.5] - 2018-07-19 ### Changed -- Make `yggdrasilctl` less case sensitive -- More verbose TCP disconnect messages +* Make `yggdrasilctl` less case sensitive +* More verbose TCP disconnect messages ### Fixed -- Fixed debug builds -- Cap maximum MTU on Linux in TAP mode -- Process successfully-read TCP traffic before checking for / handling errors (fixes EOF behavior) +* Fixed debug builds +* Cap maximum MTU on Linux in TAP mode +* Process successfully-read TCP traffic before checking for / handling errors (fixes EOF behavior) ## [0.2.4] - 2018-07-08 ### Added -- Support for UNIX domain sockets for the admin socket using `unix:///path/to/file.sock` -- Centralised platform-specific defaults +* Support for UNIX domain sockets for the admin socket using `unix:///path/to/file.sock` +* Centralised platform-specific defaults ### Changed -- Backpressure tuning, including reducing resource consumption +* Backpressure tuning, including reducing resource consumption ### Fixed -- macOS local ping bug, which previously prevented you from pinging your own `utun` adapter's IPv6 address +* macOS local ping bug, which previously prevented you from pinging your own `utun` adapter's IPv6 address ## [0.2.3] - 2018-06-29 ### Added -- Begin keeping changelog (incomplete and possibly inaccurate information before this point). -- Build RPMs in CircleCI using alien. This provides package support for Fedora, Red Hat Enterprise Linux, CentOS and other RPM-based distributions. +* Begin keeping changelog (incomplete and possibly inaccurate information before this point). +* Build RPMs in CircleCI using alien. This provides package support for Fedora, Red Hat Enterprise Linux, CentOS and other RPM-based distributions. ### Changed -- Local backpressure improvements. -- Change `box_pub_key` to `key` in admin API for simplicity. -- Session cleanup. +* Local backpressure improvements. +* Change `box_pub_key` to `key` in admin API for simplicity. +* Session cleanup. ## [0.2.2] - 2018-06-21 ### Added -- Add `yggdrasilconf` utility for testing with the `vyatta-yggdrasil` package. -- Add a randomized retry delay after TCP disconnects, to prevent synchronization livelocks. +* Add `yggdrasilconf` utility for testing with the `vyatta-yggdrasil` package. +* Add a randomized retry delay after TCP disconnects, to prevent synchronization livelocks. ### Changed -- Update build script to strip by default, which significantly reduces the size of the binary. -- Add debug `-d` and UPX `-u` flags to the `build` script. -- Start pprof in debug builds based on an environment variable (e.g. `PPROFLISTEN=localhost:6060`), instead of a flag. +* Update build script to strip by default, which significantly reduces the size of the binary. +* Add debug `-d` and UPX `-u` flags to the `build` script. +* Start pprof in debug builds based on an environment variable (e.g. `PPROFLISTEN=localhost:6060`), instead of a flag. ### Fixed -- Fix typo in big-endian BOM so that both little-endian and big-endian UTF-16 files are detected correctly. +* Fix typo in big-endian BOM so that both little-endian and big-endian UTF-16 files are detected correctly. ## [0.2.1] - 2018-06-15 ### Changed -- The address range was moved from `fd00::/8` to `200::/7`. This range was chosen as it is marked as deprecated. The change prevents overlap with other ULA privately assigned ranges. +* The address range was moved from `fd00::/8` to `200::/7`. This range was chosen as it is marked as deprecated. The change prevents overlap with other ULA privately assigned ranges. ### Fixed -- UTF-16 detection conversion for configuration files, which can particularly be a problem on Windows 10 if a configuration file is generated from within PowerShell. -- Fixes to the Debian package control file. -- Fixes to the launchd service for macOS. -- Fixes to the DHT and switch. +* UTF-16 detection conversion for configuration files, which can particularly be a problem on Windows 10 if a configuration file is generated from within PowerShell. +* Fixes to the Debian package control file. +* Fixes to the launchd service for macOS. +* Fixes to the DHT and switch. ## [0.2.0] - 2018-06-13 ### Added -- Exchange version information during connection setup, to prevent connections with incompatible versions. +* Exchange version information during connection setup, to prevent connections with incompatible versions. ### Changed -- Wire format changes (backwards incompatible). -- Less maintenance traffic per peer. -- Exponential back-off for DHT maintenance traffic (less maintenance traffic for known good peers). -- Iterative DHT (added sometime between v0.1.0 and here). -- Use local queue sizes for a sort of local-only backpressure routing, instead of the removed bandwidth estimates, when deciding where to send a packet. +* Wire format changes (backwards incompatible). +* Less maintenance traffic per peer. +* Exponential back-off for DHT maintenance traffic (less maintenance traffic for known good peers). +* Iterative DHT (added sometime between v0.1.0 and here). +* Use local queue sizes for a sort of local-only backpressure routing, instead of the removed bandwidth estimates, when deciding where to send a packet. ### Removed -- UDP peering, this may be added again if/when a better implementation appears. -- Per peer bandwidth estimation, as this has been replaced with an early local backpressure implementation. +* UDP peering, this may be added again if/when a better implementation appears. +* Per peer bandwidth estimation, as this has been replaced with an early local backpressure implementation. ## [0.1.0] - 2018-02-01 ### Added -- Adopt semantic versioning. +* Adopt semantic versioning. ### Changed -- Wire format changes (backwards incompatible). -- Many other undocumented changes leading up to this release and before the next one. +* Wire format changes (backwards incompatible). +* Many other undocumented changes leading up to this release and before the next one. ## [0.0.1] - 2017-12-28 ### Added -- First commit. -- Initial public release. +* First commit. +* Initial public release. diff --git a/README.md b/README.md index d0afae47..ea0e1a38 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ or tools in the `contrib` folder. If you want to build from source, as opposed to installing one of the pre-built packages: -1. Install [Go](https://golang.org) (requires Go 1.17 or later) +1. Install [Go](https://golang.org) (requires Go 1.22 or later) 2. Clone this repository 2. Run `./build` diff --git a/cmd/genkeys/main.go b/cmd/genkeys/main.go index 81942446..2d007cb8 100644 --- a/cmd/genkeys/main.go +++ b/cmd/genkeys/main.go @@ -1,5 +1,4 @@ /* - This file generates crypto keys. It prints out a new set of keys each time if finds a "better" one. By default, "better" means a higher NodeID (-> higher IP address). @@ -8,7 +7,6 @@ This is because the IP address format can compress leading 1s in the address, to If run with the "-sig" flag, it generates signing keys instead. A "better" signing key means one with a higher TreeID. This only matters if it's high enough to make you the root of the tree. - */ package main @@ -18,6 +16,9 @@ import ( "fmt" "net" "runtime" + "time" + + "suah.dev/protect" "github.com/yggdrasil-network/yggdrasil-go/src/address" ) @@ -25,10 +26,19 @@ import ( type keySet struct { priv ed25519.PrivateKey pub ed25519.PublicKey + count uint64 } func main() { + if err := protect.Pledge("stdio"); err != nil { + panic(err) + } + threads := runtime.GOMAXPROCS(0) + fmt.Println("Threads:", threads) + start := time.Now() + var totalKeys uint64 + totalKeys = 0 var currentBest ed25519.PublicKey newKeys := make(chan keySet, threads) for i := 0; i < threads; i++ { @@ -37,8 +47,9 @@ func main() { for { newKey := <-newKeys if isBetter(currentBest, newKey.pub) || len(currentBest) == 0 { + totalKeys += newKey.count currentBest = newKey.pub - fmt.Println("-----") + fmt.Println("-----", time.Since(start), "---", totalKeys, "keys tried") fmt.Println("Priv:", hex.EncodeToString(newKey.priv)) fmt.Println("Pub:", hex.EncodeToString(newKey.pub)) addr := address.AddrForKey(newKey.pub) @@ -61,11 +72,14 @@ func isBetter(oldPub, newPub ed25519.PublicKey) bool { func doKeys(out chan<- keySet) { bestKey := make(ed25519.PublicKey, ed25519.PublicKeySize) + var count uint64 + count = 0 for idx := range bestKey { bestKey[idx] = 0xff } for { pub, priv, err := ed25519.GenerateKey(nil) + count++ if err != nil { panic(err) } @@ -73,6 +87,7 @@ func doKeys(out chan<- keySet) { continue } bestKey = pub - out <- keySet{priv, pub} + out <- keySet{priv, pub, count} + count = 0 } } diff --git a/cmd/yggdrasil/chuser_other.go b/cmd/yggdrasil/chuser_other.go new file mode 100644 index 00000000..702f3715 --- /dev/null +++ b/cmd/yggdrasil/chuser_other.go @@ -0,0 +1,10 @@ +//go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris +// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris + +package main + +import "errors" + +func chuser(user string) error { + return errors.New("setting uid/gid is not supported on this platform") +} diff --git a/cmd/yggdrasil/chuser_unix.go b/cmd/yggdrasil/chuser_unix.go new file mode 100644 index 00000000..24a706df --- /dev/null +++ b/cmd/yggdrasil/chuser_unix.go @@ -0,0 +1,63 @@ +//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris +// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris + +package main + +import ( + "fmt" + "os/user" + "strconv" + "strings" + + "golang.org/x/sys/unix" +) + +func chuser(input string) error { + givenUser, givenGroup, _ := strings.Cut(input, ":") + if givenUser == "" { + return fmt.Errorf("user is empty") + } + if strings.Contains(input, ":") && givenGroup == "" { + return fmt.Errorf("group is empty") + } + + var ( + err error + usr *user.User + grp *user.Group + uid, gid int + ) + + if usr, err = user.LookupId(givenUser); err != nil { + if usr, err = user.Lookup(givenUser); err != nil { + return err + } + } + if uid, err = strconv.Atoi(usr.Uid); err != nil { + return err + } + + if givenGroup != "" { + if grp, err = user.LookupGroupId(givenGroup); err != nil { + if grp, err = user.LookupGroup(givenGroup); err != nil { + return err + } + } + + gid, _ = strconv.Atoi(grp.Gid) + } else { + gid, _ = strconv.Atoi(usr.Gid) + } + + if err := unix.Setgroups([]int{gid}); err != nil { + return fmt.Errorf("setgroups: %d: %v", gid, err) + } + if err := unix.Setgid(gid); err != nil { + return fmt.Errorf("setgid: %d: %v", gid, err) + } + if err := unix.Setuid(uid); err != nil { + return fmt.Errorf("setuid: %d: %v", uid, err) + } + + return nil +} diff --git a/cmd/yggdrasil/chuser_unix_test.go b/cmd/yggdrasil/chuser_unix_test.go new file mode 100644 index 00000000..fc624ac2 --- /dev/null +++ b/cmd/yggdrasil/chuser_unix_test.go @@ -0,0 +1,80 @@ +//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris +// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris + +package main + +import ( + "os/user" + "testing" +) + +// Usernames must not contain a number sign. +func TestEmptyString(t *testing.T) { + if chuser("") == nil { + t.Fatal("the empty string is not a valid user") + } +} + +// Either omit delimiter and group, or omit both. +func TestEmptyGroup(t *testing.T) { + if chuser("0:") == nil { + t.Fatal("the empty group is not allowed") + } +} + +// Either user only or user and group. +func TestGroupOnly(t *testing.T) { + if chuser(":0") == nil { + t.Fatal("group only is not allowed") + } +} + +// Usenames must not contain the number sign. +func TestInvalidUsername(t *testing.T) { + const username = "#user" + if chuser(username) == nil { + t.Fatalf("'%s' is not a valid username", username) + } +} + +// User IDs must be non-negative. +func TestInvalidUserid(t *testing.T) { + if chuser("-1") == nil { + t.Fatal("User ID cannot be negative") + } +} + +// Change to the current user by ID. +func TestCurrentUserid(t *testing.T) { + usr, err := user.Current() + if err != nil { + t.Fatal(err) + } + + if usr.Uid != "0" { + t.Skip("setgroups(2): Only the superuser may set new groups.") + } + + if err = chuser(usr.Uid); err != nil { + t.Fatal(err) + } +} + +// Change to a common user by name. +func TestCommonUsername(t *testing.T) { + usr, err := user.Current() + if err != nil { + t.Fatal(err) + } + + if usr.Uid != "0" { + t.Skip("setgroups(2): Only the superuser may set new groups.") + } + + if err := chuser("nobody"); err != nil { + if _, ok := err.(user.UnknownUserError); ok { + t.Skip(err) + } + t.Fatal(err) + } +} diff --git a/cmd/yggdrasil/main.go b/cmd/yggdrasil/main.go index 58b8230d..b3c9151d 100644 --- a/cmd/yggdrasil/main.go +++ b/cmd/yggdrasil/main.go @@ -1,159 +1,340 @@ package main import ( - "bytes" "context" "crypto/ed25519" "encoding/hex" "encoding/json" "flag" "fmt" - "io/ioutil" "net" "os" "os/signal" + "regexp" "strings" "syscall" - "golang.org/x/text/encoding/unicode" + "suah.dev/protect" "github.com/gologme/log" gsyslog "github.com/hashicorp/go-syslog" - "github.com/hjson/hjson-go" + "github.com/hjson/hjson-go/v4" "github.com/kardianos/minwinsvc" - "github.com/mitchellh/mapstructure" "github.com/yggdrasil-network/yggdrasil-go/src/address" "github.com/yggdrasil-network/yggdrasil-go/src/admin" "github.com/yggdrasil-network/yggdrasil-go/src/config" - "github.com/yggdrasil-network/yggdrasil-go/src/defaults" + "github.com/yggdrasil-network/yggdrasil-go/src/ipv6rwc" "github.com/yggdrasil-network/yggdrasil-go/src/core" - "github.com/yggdrasil-network/yggdrasil-go/src/ipv6rwc" "github.com/yggdrasil-network/yggdrasil-go/src/multicast" - "github.com/yggdrasil-network/yggdrasil-go/src/tuntap" + "github.com/yggdrasil-network/yggdrasil-go/src/tun" "github.com/yggdrasil-network/yggdrasil-go/src/version" ) type node struct { - core core.Core - config *config.NodeConfig - tuntap *tuntap.TunAdapter + core *core.Core + tun *tun.TunAdapter multicast *multicast.Multicast admin *admin.AdminSocket } -func readConfig(log *log.Logger, useconf bool, useconffile string, normaliseconf bool) *config.NodeConfig { - // Use a configuration file. If -useconf, the configuration will be read - // from stdin. If -useconffile, the configuration will be read from the - // filesystem. - var conf []byte - var err error - if useconffile != "" { - // Read the file from the filesystem - conf, err = ioutil.ReadFile(useconffile) +// The main function is responsible for configuring and starting Yggdrasil. +func main() { + // Not all operations are coverable with pledge(2), so immediately + // limit file system access with unveil(2), effectively preventing + // "proc exec" promises right from the start: + // + // - read arbitrary config file + // - create/write arbitrary log file + // - read/write/chmod/remove admin socket, if at all + if err := protect.Unveil("/", "rwc"); err != nil { + panic(fmt.Sprintf("unveil: / rwc: %v", err)) + } + if err := protect.UnveilBlock(); err != nil { + panic(fmt.Sprintf("unveil: %v", err)) + } + + genconf := flag.Bool("genconf", false, "print a new config to stdout") + useconf := flag.Bool("useconf", false, "read HJSON/JSON config from stdin") + useconffile := flag.String("useconffile", "", "read HJSON/JSON config from specified file path") + normaliseconf := flag.Bool("normaliseconf", false, "use in combination with either -useconf or -useconffile, outputs your configuration normalised") + exportkey := flag.Bool("exportkey", false, "use in combination with either -useconf or -useconffile, outputs your private key in PEM format") + confjson := flag.Bool("json", false, "print configuration from -genconf or -normaliseconf as JSON instead of HJSON") + autoconf := flag.Bool("autoconf", false, "automatic mode (dynamic IP, peer with IPv6 neighbors)") + ver := flag.Bool("version", false, "prints the version of this build") + logto := flag.String("logto", "stdout", "file path to log to, \"syslog\" or \"stdout\"") + getaddr := flag.Bool("address", false, "use in combination with either -useconf or -useconffile, outputs your IPv6 address") + getsnet := flag.Bool("subnet", false, "use in combination with either -useconf or -useconffile, outputs your IPv6 subnet") + getpkey := flag.Bool("publickey", false, "use in combination with either -useconf or -useconffile, outputs your public key") + loglevel := flag.String("loglevel", "info", "loglevel to enable") + chuserto := flag.String("user", "", "user (and, optionally, group) to set UID/GID to") + flag.Parse() + + done := make(chan struct{}) + defer close(done) + + // Catch interrupts from the operating system to exit gracefully. + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + + // Create a new logger that logs output to stdout. + var logger *log.Logger + switch *logto { + case "stdout": + logger = log.New(os.Stdout, "", log.Flags()) + + case "syslog": + if syslogger, err := gsyslog.NewLogger(gsyslog.LOG_NOTICE, "DAEMON", version.BuildName()); err == nil { + logger = log.New(syslogger, "", log.Flags()&^(log.Ldate|log.Ltime)) + } + + default: + if logfd, err := os.OpenFile(*logto, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil { + logger = log.New(logfd, "", log.Flags()) + } + } + if logger == nil { + logger = log.New(os.Stdout, "", log.Flags()) + logger.Warnln("Logging defaulting to stdout") + } + if *normaliseconf { + setLogLevel("error", logger) } else { - // Read the file from stdin. - conf, err = ioutil.ReadAll(os.Stdin) + setLogLevel(*loglevel, logger) } - if err != nil { - panic(err) + + cfg := config.GenerateConfig() + var err error + switch { + case *ver: + fmt.Println("Build name:", version.BuildName()) + fmt.Println("Build version:", version.BuildVersion()) + return + + case *autoconf: + // Use an autoconf-generated config, this will give us random keys and + // port numbers, and will use an automatically selected TUN interface. + + case *useconf: + if _, err := cfg.ReadFrom(os.Stdin); err != nil { + panic(err) + } + + case *useconffile != "": + f, err := os.Open(*useconffile) + if err != nil { + panic(err) + } + if _, err := cfg.ReadFrom(f); err != nil { + panic(err) + } + _ = f.Close() + + case *genconf: + cfg.AdminListen = "" + var bs []byte + if *confjson { + bs, err = json.MarshalIndent(cfg, "", " ") + } else { + bs, err = hjson.Marshal(cfg) + } + if err != nil { + panic(err) + } + fmt.Println(string(bs)) + return + + default: + fmt.Println("Usage:") + flag.PrintDefaults() + + if *getaddr || *getsnet { + fmt.Println("\nError: You need to specify some config data using -useconf or -useconffile.") + } + return } - // If there's a byte order mark - which Windows 10 is now incredibly fond of - // throwing everywhere when it's converting things into UTF-16 for the hell - // of it - remove it and decode back down into UTF-8. This is necessary - // because hjson doesn't know what to do with UTF-16 and will panic - if bytes.Equal(conf[0:2], []byte{0xFF, 0xFE}) || - bytes.Equal(conf[0:2], []byte{0xFE, 0xFF}) { - utf := unicode.UTF16(unicode.BigEndian, unicode.UseBOM) - decoder := utf.NewDecoder() - conf, err = decoder.Bytes(conf) + + privateKey := ed25519.PrivateKey(cfg.PrivateKey) + publicKey := privateKey.Public().(ed25519.PublicKey) + + switch { + case *getaddr: + addr := address.AddrForKey(publicKey) + ip := net.IP(addr[:]) + fmt.Println(ip.String()) + return + + case *getsnet: + snet := address.SubnetForKey(publicKey) + ipnet := net.IPNet{ + IP: append(snet[:], 0, 0, 0, 0, 0, 0, 0, 0), + Mask: net.CIDRMask(len(snet)*8, 128), + } + fmt.Println(ipnet.String()) + return + + case *getpkey: + fmt.Println(hex.EncodeToString(publicKey)) + return + + case *normaliseconf: + cfg.AdminListen = "" + if cfg.PrivateKeyPath != "" { + cfg.PrivateKey = nil + } + var bs []byte + if *confjson { + bs, err = json.MarshalIndent(cfg, "", " ") + } else { + bs, err = hjson.Marshal(cfg) + } + if err != nil { + panic(err) + } + fmt.Println(string(bs)) + return + + case *exportkey: + pem, err := cfg.MarshalPEMPrivateKey() + if err != nil { + panic(err) + } + fmt.Println(string(pem)) + return + } + + n := &node{} + + // Set up the Yggdrasil node itself. + { + iprange := net.IPNet{ + IP: net.ParseIP("200::"), + Mask: net.CIDRMask(7, 128), + } + options := []core.SetupOption{ + core.NodeInfo(cfg.NodeInfo), + core.NodeInfoPrivacy(cfg.NodeInfoPrivacy), + core.PeerFilter(func(ip net.IP) bool { + return !iprange.Contains(ip) + }), + } + for _, addr := range cfg.Listen { + options = append(options, core.ListenAddress(addr)) + } + for _, peer := range cfg.Peers { + options = append(options, core.Peer{URI: peer}) + } + for intf, peers := range cfg.InterfacePeers { + for _, peer := range peers { + options = append(options, core.Peer{URI: peer, SourceInterface: intf}) + } + } + for _, allowed := range cfg.AllowedPublicKeys { + k, err := hex.DecodeString(allowed) + if err != nil { + panic(err) + } + options = append(options, core.AllowedPublicKey(k[:])) + } + if n.core, err = core.New(cfg.Certificate, logger, options...); err != nil { + panic(err) + } + address, subnet := n.core.Address(), n.core.Subnet() + logger.Printf("Your public key is %s", hex.EncodeToString(n.core.PublicKey())) + logger.Printf("Your IPv6 address is %s", address.String()) + logger.Printf("Your IPv6 subnet is %s", subnet.String()) + } + + // Set up the admin socket. + { + options := []admin.SetupOption{ + admin.ListenAddress(cfg.AdminListen), + } + if cfg.LogLookups { + options = append(options, admin.LogLookups{}) + } + if n.admin, err = admin.New(n.core, logger, options...); err != nil { + panic(err) + } + if n.admin != nil { + n.admin.SetupAdminHandlers() + } + } + + // Set up the multicast module. + { + options := []multicast.SetupOption{} + for _, intf := range cfg.MulticastInterfaces { + options = append(options, multicast.MulticastInterface{ + Regex: regexp.MustCompile(intf.Regex), + Beacon: intf.Beacon, + Listen: intf.Listen, + Port: intf.Port, + Priority: uint8(intf.Priority), + Password: intf.Password, + }) + } + if n.multicast, err = multicast.New(n.core, logger, options...); err != nil { + panic(err) + } + if n.admin != nil && n.multicast != nil { + n.multicast.SetupAdminHandlers(n.admin) + } + } + + // Set up the TUN module. + { + options := []tun.SetupOption{ + tun.InterfaceName(cfg.IfName), + tun.InterfaceMTU(cfg.IfMTU), + } + if n.tun, err = tun.New(ipv6rwc.NewReadWriteCloser(n.core), logger, options...); err != nil { + panic(err) + } + if n.admin != nil && n.tun != nil { + n.tun.SetupAdminHandlers(n.admin) + } + } + + //Windows service shutdown + minwinsvc.SetOnExit(func() { + logger.Infof("Shutting down service ...") + cancel() + // Wait for all parts to shutdown properly + <-done + }) + + // Change user if requested + if *chuserto != "" { + err = chuser(*chuserto) if err != nil { panic(err) } } - // Generate a new configuration - this gives us a set of sane defaults - - // then parse the configuration we loaded above on top of it. The effect - // of this is that any configuration item that is missing from the provided - // configuration will use a sane default. - cfg := defaults.GenerateConfig() - var dat map[string]interface{} - if err := hjson.Unmarshal(conf, &dat); err != nil { - panic(err) - } - // Check if we have old field names - if _, ok := dat["TunnelRouting"]; ok { - log.Warnln("WARNING: Tunnel routing is no longer supported") - } - if old, ok := dat["SigningPrivateKey"]; ok { - log.Warnln("WARNING: The \"SigningPrivateKey\" configuration option has been renamed to \"PrivateKey\"") - if _, ok := dat["PrivateKey"]; !ok { - if privstr, err := hex.DecodeString(old.(string)); err == nil { - priv := ed25519.PrivateKey(privstr) - pub := priv.Public().(ed25519.PublicKey) - dat["PrivateKey"] = hex.EncodeToString(priv[:]) - dat["PublicKey"] = hex.EncodeToString(pub[:]) - } else { - log.Warnln("WARNING: The \"SigningPrivateKey\" configuration option contains an invalid value and will be ignored") - } - } - } - if oldmc, ok := dat["MulticastInterfaces"]; ok { - if oldmcvals, ok := oldmc.([]interface{}); ok { - var newmc []config.MulticastInterfaceConfig - for _, oldmcval := range oldmcvals { - if str, ok := oldmcval.(string); ok { - newmc = append(newmc, config.MulticastInterfaceConfig{ - Regex: str, - Beacon: true, - Listen: true, - }) - } - } - if newmc != nil { - if oldport, ok := dat["LinkLocalTCPPort"]; ok { - // numbers parse to float64 by default - if port, ok := oldport.(float64); ok { - for idx := range newmc { - newmc[idx].Port = uint16(port) - } - } - } - dat["MulticastInterfaces"] = newmc - } - } - } - // Sanitise the config - confJson, err := json.Marshal(dat) - if err != nil { - panic(err) - } - if err := json.Unmarshal(confJson, &cfg); err != nil { - panic(err) - } - // Overlay our newly mapped configuration onto the autoconf node config that - // we generated above. - if err = mapstructure.Decode(dat, &cfg); err != nil { - panic(err) - } - return cfg -} -// Generates a new configuration and returns it in HJSON format. This is used -// with -genconf. -func doGenconf(isjson bool) string { - cfg := defaults.GenerateConfig() - var bs []byte - var err error - if isjson { - bs, err = json.MarshalIndent(cfg, "", " ") - } else { - bs, err = hjson.Marshal(cfg) + // Promise final modes of operation. At this point, if at all: + // - raw socket is created/open + // - admin socket is created/open + // - privileges are dropped to non-root user + // + // Peers, InterfacePeers, Listen can be UNIX sockets; + // Go's net.Listen.Close() deletes files on shutdown. + promises := []string{"stdio", "cpath", "inet", "unix", "dns"} + if len(cfg.MulticastInterfaces) > 0 { + promises = append(promises, "mcast") } - if err != nil { - panic(err) + if err := protect.Pledge(strings.Join(promises, " ")); err != nil { + panic(fmt.Sprintf("pledge: %v: %v", promises, err)) } - return string(bs) + + // Block until we are told to shut down. + <-ctx.Done() + + // Shut down the node. + _ = n.admin.Stop() + _ = n.multicast.Stop() + _ = n.tun.Stop() + n.core.Stop() } func setLogLevel(loglevel string, logger *log.Logger) { @@ -181,228 +362,3 @@ func setLogLevel(loglevel string, logger *log.Logger) { } } } - -type yggArgs struct { - genconf bool - useconf bool - normaliseconf bool - confjson bool - autoconf bool - ver bool - getaddr bool - getsnet bool - useconffile string - logto string - loglevel string -} - -func getArgs() yggArgs { - genconf := flag.Bool("genconf", false, "print a new config to stdout") - useconf := flag.Bool("useconf", false, "read HJSON/JSON config from stdin") - useconffile := flag.String("useconffile", "", "read HJSON/JSON config from specified file path") - normaliseconf := flag.Bool("normaliseconf", false, "use in combination with either -useconf or -useconffile, outputs your configuration normalised") - confjson := flag.Bool("json", false, "print configuration from -genconf or -normaliseconf as JSON instead of HJSON") - autoconf := flag.Bool("autoconf", false, "automatic mode (dynamic IP, peer with IPv6 neighbors)") - ver := flag.Bool("version", false, "prints the version of this build") - logto := flag.String("logto", "stdout", "file path to log to, \"syslog\" or \"stdout\"") - getaddr := flag.Bool("address", false, "returns the IPv6 address as derived from the supplied configuration") - getsnet := flag.Bool("subnet", false, "returns the IPv6 subnet as derived from the supplied configuration") - loglevel := flag.String("loglevel", "info", "loglevel to enable") - flag.Parse() - return yggArgs{ - genconf: *genconf, - useconf: *useconf, - useconffile: *useconffile, - normaliseconf: *normaliseconf, - confjson: *confjson, - autoconf: *autoconf, - ver: *ver, - logto: *logto, - getaddr: *getaddr, - getsnet: *getsnet, - loglevel: *loglevel, - } -} - -// The main function is responsible for configuring and starting Yggdrasil. -func run(args yggArgs, ctx context.Context, done chan struct{}) { - defer close(done) - // Create a new logger that logs output to stdout. - var logger *log.Logger - switch args.logto { - case "stdout": - logger = log.New(os.Stdout, "", log.Flags()) - case "syslog": - if syslogger, err := gsyslog.NewLogger(gsyslog.LOG_NOTICE, "DAEMON", version.BuildName()); err == nil { - logger = log.New(syslogger, "", log.Flags()) - } - default: - if logfd, err := os.OpenFile(args.logto, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil { - logger = log.New(logfd, "", log.Flags()) - } - } - if logger == nil { - logger = log.New(os.Stdout, "", log.Flags()) - logger.Warnln("Logging defaulting to stdout") - } - - if args.normaliseconf { - setLogLevel("error", logger) - } else { - setLogLevel(args.loglevel, logger) - } - - var cfg *config.NodeConfig - var err error - switch { - case args.ver: - fmt.Println("Build name:", version.BuildName()) - fmt.Println("Build version:", version.BuildVersion()) - return - case args.autoconf: - // Use an autoconf-generated config, this will give us random keys and - // port numbers, and will use an automatically selected TUN/TAP interface. - cfg = defaults.GenerateConfig() - case args.useconffile != "" || args.useconf: - // Read the configuration from either stdin or from the filesystem - cfg = readConfig(logger, args.useconf, args.useconffile, args.normaliseconf) - // If the -normaliseconf option was specified then remarshal the above - // configuration and print it back to stdout. This lets the user update - // their configuration file with newly mapped names (like above) or to - // convert from plain JSON to commented HJSON. - if args.normaliseconf { - var bs []byte - if args.confjson { - bs, err = json.MarshalIndent(cfg, "", " ") - } else { - bs, err = hjson.Marshal(cfg) - } - if err != nil { - panic(err) - } - fmt.Println(string(bs)) - return - } - case args.genconf: - // Generate a new configuration and print it to stdout. - fmt.Println(doGenconf(args.confjson)) - return - default: - // No flags were provided, therefore print the list of flags to stdout. - flag.PrintDefaults() - } - // Have we got a working configuration? If we don't then it probably means - // that neither -autoconf, -useconf or -useconffile were set above. Stop - // if we don't. - if cfg == nil { - return - } - // Have we been asked for the node address yet? If so, print it and then stop. - getNodeKey := func() ed25519.PublicKey { - if pubkey, err := hex.DecodeString(cfg.PrivateKey); err == nil { - return ed25519.PrivateKey(pubkey).Public().(ed25519.PublicKey) - } - return nil - } - switch { - case args.getaddr: - if key := getNodeKey(); key != nil { - addr := address.AddrForKey(key) - ip := net.IP(addr[:]) - fmt.Println(ip.String()) - } - return - case args.getsnet: - if key := getNodeKey(); key != nil { - snet := address.SubnetForKey(key) - ipnet := net.IPNet{ - IP: append(snet[:], 0, 0, 0, 0, 0, 0, 0, 0), - Mask: net.CIDRMask(len(snet)*8, 128), - } - fmt.Println(ipnet.String()) - } - return - default: - } - - // Setup the Yggdrasil node itself. The node{} type includes a Core, so we - // don't need to create this manually. - n := node{config: cfg} - // Now start Yggdrasil - this starts the DHT, router, switch and other core - // components needed for Yggdrasil to operate - if err = n.core.Start(cfg, logger); err != nil { - logger.Errorln("An error occurred during startup") - panic(err) - } - // Register the session firewall gatekeeper function - // Allocate our modules - n.admin = &admin.AdminSocket{} - n.multicast = &multicast.Multicast{} - n.tuntap = &tuntap.TunAdapter{} - // Start the admin socket - if err := n.admin.Init(&n.core, cfg, logger, nil); err != nil { - logger.Errorln("An error occurred initialising admin socket:", err) - } else if err := n.admin.Start(); err != nil { - logger.Errorln("An error occurred starting admin socket:", err) - } - n.admin.SetupAdminHandlers(n.admin) - // Start the multicast interface - if err := n.multicast.Init(&n.core, cfg, logger, nil); err != nil { - logger.Errorln("An error occurred initialising multicast:", err) - } else if err := n.multicast.Start(); err != nil { - logger.Errorln("An error occurred starting multicast:", err) - } - n.multicast.SetupAdminHandlers(n.admin) - // Start the TUN/TAP interface - rwc := ipv6rwc.NewReadWriteCloser(&n.core) - if err := n.tuntap.Init(rwc, cfg, logger, nil); err != nil { - logger.Errorln("An error occurred initialising TUN/TAP:", err) - } else if err := n.tuntap.Start(); err != nil { - logger.Errorln("An error occurred starting TUN/TAP:", err) - } - n.tuntap.SetupAdminHandlers(n.admin) - // Make some nice output that tells us what our IPv6 address and subnet are. - // This is just logged to stdout for the user. - address := n.core.Address() - subnet := n.core.Subnet() - public := n.core.GetSelf().Key - logger.Infof("Your public key is %s", hex.EncodeToString(public[:])) - logger.Infof("Your IPv6 address is %s", address.String()) - logger.Infof("Your IPv6 subnet is %s", subnet.String()) - // Catch interrupts from the operating system to exit gracefully. - <-ctx.Done() - // Capture the service being stopped on Windows. - minwinsvc.SetOnExit(n.shutdown) - n.shutdown() -} - -func (n *node) shutdown() { - _ = n.admin.Stop() - _ = n.multicast.Stop() - _ = n.tuntap.Stop() - n.core.Stop() -} - -func main() { - args := getArgs() - hup := make(chan os.Signal, 1) - //signal.Notify(hup, os.Interrupt, syscall.SIGHUP) - term := make(chan os.Signal, 1) - signal.Notify(term, os.Interrupt, syscall.SIGTERM) - for { - done := make(chan struct{}) - ctx, cancel := context.WithCancel(context.Background()) - go run(args, ctx, done) - select { - case <-hup: - cancel() - <-done - case <-term: - cancel() - <-done - return - case <-done: - return - } - } -} diff --git a/cmd/yggdrasilctl/cmd_line_env.go b/cmd/yggdrasilctl/cmd_line_env.go index bd6df8fc..e929b0ba 100644 --- a/cmd/yggdrasilctl/cmd_line_env.go +++ b/cmd/yggdrasilctl/cmd_line_env.go @@ -4,25 +4,24 @@ import ( "bytes" "flag" "fmt" - "io/ioutil" "log" "os" - "github.com/hjson/hjson-go" + "github.com/hjson/hjson-go/v4" "golang.org/x/text/encoding/unicode" - "github.com/yggdrasil-network/yggdrasil-go/src/defaults" + "github.com/yggdrasil-network/yggdrasil-go/src/config" ) type CmdLineEnv struct { - args []string - endpoint, server string - injson, verbose, ver bool + args []string + endpoint, server string + injson, ver bool } func newCmdLineEnv() CmdLineEnv { var cmdLineEnv CmdLineEnv - cmdLineEnv.endpoint = defaults.GetDefaults().DefaultAdminListen + cmdLineEnv.endpoint = config.GetDefaults().DefaultAdminListen return cmdLineEnv } @@ -39,15 +38,12 @@ func (cmdLineEnv *CmdLineEnv) parseFlagsAndArgs() { fmt.Println("Examples:") fmt.Println(" - ", os.Args[0], "list") fmt.Println(" - ", os.Args[0], "getPeers") - fmt.Println(" - ", os.Args[0], "-v getSelf") - fmt.Println(" - ", os.Args[0], "setTunTap name=auto mtu=1500 tap_mode=false") - fmt.Println(" - ", os.Args[0], "-endpoint=tcp://localhost:9001 getDHT") - fmt.Println(" - ", os.Args[0], "-endpoint=unix:///var/run/ygg.sock getDHT") + fmt.Println(" - ", os.Args[0], "-endpoint=tcp://localhost:9001 getPeers") + fmt.Println(" - ", os.Args[0], "-endpoint=unix:///var/run/ygg.sock getPeers") } server := flag.String("endpoint", cmdLineEnv.endpoint, "Admin socket endpoint") injson := flag.Bool("json", false, "Output in JSON format (as opposed to pretty-print)") - verbose := flag.Bool("v", false, "Verbose output (includes public keys)") ver := flag.Bool("version", false, "Prints the version of this build") flag.Parse() @@ -55,37 +51,36 @@ func (cmdLineEnv *CmdLineEnv) parseFlagsAndArgs() { cmdLineEnv.args = flag.Args() cmdLineEnv.server = *server cmdLineEnv.injson = *injson - cmdLineEnv.verbose = *verbose cmdLineEnv.ver = *ver } func (cmdLineEnv *CmdLineEnv) setEndpoint(logger *log.Logger) { if cmdLineEnv.server == cmdLineEnv.endpoint { - if config, err := ioutil.ReadFile(defaults.GetDefaults().DefaultConfigFile); err == nil { - if bytes.Equal(config[0:2], []byte{0xFF, 0xFE}) || - bytes.Equal(config[0:2], []byte{0xFE, 0xFF}) { + if cfg, err := os.ReadFile(config.GetDefaults().DefaultConfigFile); err == nil { + if bytes.Equal(cfg[0:2], []byte{0xFF, 0xFE}) || + bytes.Equal(cfg[0:2], []byte{0xFE, 0xFF}) { utf := unicode.UTF16(unicode.BigEndian, unicode.UseBOM) decoder := utf.NewDecoder() - config, err = decoder.Bytes(config) + cfg, err = decoder.Bytes(cfg) if err != nil { panic(err) } } var dat map[string]interface{} - if err := hjson.Unmarshal(config, &dat); err != nil { + if err := hjson.Unmarshal(cfg, &dat); err != nil { panic(err) } if ep, ok := dat["AdminListen"].(string); ok && (ep != "none" && ep != "") { cmdLineEnv.endpoint = ep - logger.Println("Found platform default config file", defaults.GetDefaults().DefaultConfigFile) + logger.Println("Found platform default config file", config.GetDefaults().DefaultConfigFile) logger.Println("Using endpoint", cmdLineEnv.endpoint, "from AdminListen") } else { logger.Println("Configuration file doesn't contain appropriate AdminListen option") - logger.Println("Falling back to platform default", defaults.GetDefaults().DefaultAdminListen) + logger.Println("Falling back to platform default", config.GetDefaults().DefaultAdminListen) } } else { - logger.Println("Can't open config file from default location", defaults.GetDefaults().DefaultConfigFile) - logger.Println("Falling back to platform default", defaults.GetDefaults().DefaultAdminListen) + logger.Println("Can't open config file from default location", config.GetDefaults().DefaultConfigFile) + logger.Println("Falling back to platform default", config.GetDefaults().DefaultAdminListen) } } else { cmdLineEnv.endpoint = cmdLineEnv.server diff --git a/cmd/yggdrasilctl/main.go b/cmd/yggdrasilctl/main.go index 180bea09..51c25dcd 100644 --- a/cmd/yggdrasilctl/main.go +++ b/cmd/yggdrasilctl/main.go @@ -10,16 +10,25 @@ import ( "net" "net/url" "os" - "sort" - "strconv" "strings" + "time" + "suah.dev/protect" + + "github.com/olekukonko/tablewriter" + "github.com/yggdrasil-network/yggdrasil-go/src/admin" + "github.com/yggdrasil-network/yggdrasil-go/src/core" + "github.com/yggdrasil-network/yggdrasil-go/src/multicast" + "github.com/yggdrasil-network/yggdrasil-go/src/tun" "github.com/yggdrasil-network/yggdrasil-go/src/version" ) -type admin_info map[string]interface{} - func main() { + // read config, speak DNS/TCP and/or over a UNIX socket + if err := protect.Pledge("stdio rpath inet unix dns"); err != nil { + panic(err) + } + // makes sure we can use defer and still return an error code to the OS os.Exit(run()) } @@ -54,103 +63,13 @@ func run() int { cmdLineEnv.setEndpoint(logger) - conn := connect(cmdLineEnv.endpoint, logger) - logger.Println("Connected") - defer conn.Close() - - decoder := json.NewDecoder(conn) - encoder := json.NewEncoder(conn) - send := make(admin_info) - recv := make(admin_info) - - for c, a := range cmdLineEnv.args { - if c == 0 { - if strings.HasPrefix(a, "-") { - logger.Printf("Ignoring flag %s as it should be specified before other parameters\n", a) - continue - } - logger.Printf("Sending request: %v\n", a) - send["request"] = a - continue - } - tokens := strings.Split(a, "=") - if len(tokens) == 1 { - send[tokens[0]] = true - } else if len(tokens) > 2 { - send[tokens[0]] = strings.Join(tokens[1:], "=") - } else if len(tokens) == 2 { - if i, err := strconv.Atoi(tokens[1]); err == nil { - logger.Printf("Sending parameter %s: %d\n", tokens[0], i) - send[tokens[0]] = i - } else { - switch strings.ToLower(tokens[1]) { - case "true": - send[tokens[0]] = true - case "false": - send[tokens[0]] = false - default: - send[tokens[0]] = tokens[1] - } - logger.Printf("Sending parameter %s: %v\n", tokens[0], send[tokens[0]]) - } - } - } - - if err := encoder.Encode(&send); err != nil { - panic(err) - } - - logger.Printf("Request sent") - - if err := decoder.Decode(&recv); err == nil { - logger.Printf("Response received") - if recv["status"] == "error" { - if err, ok := recv["error"]; ok { - fmt.Println("Admin socket returned an error:", err) - } else { - fmt.Println("Admin socket returned an error but didn't specify any error text") - } - return 1 - } - if _, ok := recv["request"]; !ok { - fmt.Println("Missing request in response (malformed response?)") - return 1 - } - if _, ok := recv["response"]; !ok { - fmt.Println("Missing response body (malformed response?)") - return 1 - } - res := recv["response"].(map[string]interface{}) - - if cmdLineEnv.injson { - if json, err := json.MarshalIndent(res, "", " "); err == nil { - fmt.Println(string(json)) - } - return 0 - } - - handleAll(recv, cmdLineEnv.verbose) - } else { - logger.Println("Error receiving response:", err) - } - - if v, ok := recv["status"]; ok && v != "success" { - return 1 - } - - return 0 -} - -func connect(endpoint string, logger *log.Logger) net.Conn { var conn net.Conn - - u, err := url.Parse(endpoint) - + u, err := url.Parse(cmdLineEnv.endpoint) if err == nil { switch strings.ToLower(u.Scheme) { case "unix": - logger.Println("Connecting to UNIX socket", endpoint[7:]) - conn, err = net.Dial("unix", endpoint[7:]) + logger.Println("Connecting to UNIX socket", cmdLineEnv.endpoint[7:]) + conn, err = net.Dial("unix", cmdLineEnv.endpoint[7:]) case "tcp": logger.Println("Connecting to TCP socket", u.Host) conn, err = net.Dial("tcp", u.Host) @@ -160,298 +79,255 @@ func connect(endpoint string, logger *log.Logger) net.Conn { } } else { logger.Println("Connecting to TCP socket", u.Host) - conn, err = net.Dial("tcp", endpoint) + conn, err = net.Dial("tcp", cmdLineEnv.endpoint) } - if err != nil { panic(err) } - return conn -} + // config and socket are done, work without unprivileges + if err := protect.Pledge("stdio"); err != nil { + panic(err) + } -func handleAll(recv map[string]interface{}, verbose bool) { - req := recv["request"].(map[string]interface{}) - res := recv["response"].(map[string]interface{}) + logger.Println("Connected") + defer conn.Close() - switch strings.ToLower(req["request"].(string)) { - case "dot": - handleDot(res) - case "list", "getpeers", "getswitchpeers", "getdht", "getsessions", "dhtping": - handleVariousInfo(res, verbose) - case "gettuntap", "settuntap": - handleGetAndSetTunTap(res) - case "getself": - handleGetSelf(res, verbose) - case "getswitchqueues": - handleGetSwitchQueues(res) - case "addpeer", "removepeer", "addallowedencryptionpublickey", "removeallowedencryptionpublickey", "addsourcesubnet", "addroute", "removesourcesubnet", "removeroute": - handleAddsAndRemoves(res) - case "getallowedencryptionpublickeys": - handleGetAllowedEncryptionPublicKeys(res) - case "getmulticastinterfaces": - handleGetMulticastInterfaces(res) - case "getsourcesubnets": - handleGetSourceSubnets(res) - case "getroutes": - handleGetRoutes(res) - case "settunnelrouting": - fallthrough - case "gettunnelrouting": - handleGetTunnelRouting(res) - default: - if json, err := json.MarshalIndent(recv["response"], "", " "); err == nil { + decoder := json.NewDecoder(conn) + encoder := json.NewEncoder(conn) + send := &admin.AdminSocketRequest{} + recv := &admin.AdminSocketResponse{} + args := map[string]string{} + for c, a := range cmdLineEnv.args { + if c == 0 { + if strings.HasPrefix(a, "-") { + logger.Printf("Ignoring flag %s as it should be specified before other parameters\n", a) + continue + } + logger.Printf("Sending request: %v\n", a) + send.Name = a + continue + } + tokens := strings.SplitN(a, "=", 2) + switch { + case len(tokens) == 1: + logger.Println("Ignoring invalid argument:", a) + default: + args[tokens[0]] = tokens[1] + } + } + if send.Arguments, err = json.Marshal(args); err != nil { + panic(err) + } + if err := encoder.Encode(&send); err != nil { + panic(err) + } + logger.Printf("Request sent") + if err := decoder.Decode(&recv); err != nil { + panic(err) + } + if recv.Status == "error" { + if err := recv.Error; err != "" { + fmt.Println("Admin socket returned an error:", err) + } else { + fmt.Println("Admin socket returned an error but didn't specify any error text") + } + return 1 + } + if cmdLineEnv.injson { + if json, err := json.MarshalIndent(recv.Response, "", " "); err == nil { fmt.Println(string(json)) } + return 0 } -} -func handleDot(res map[string]interface{}) { - fmt.Println(res["dot"]) -} + table := tablewriter.NewWriter(os.Stdout) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetAutoFormatHeaders(false) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetHeaderLine(false) + table.SetBorder(false) + table.SetTablePadding("\t") // pad with tabs + table.SetNoWhiteSpace(true) + table.SetAutoWrapText(false) -func handleVariousInfo(res map[string]interface{}, verbose bool) { - maxWidths := make(map[string]int) - var keyOrder []string - keysOrdered := false - - for _, tlv := range res { - for slk, slv := range tlv.(map[string]interface{}) { - if !keysOrdered { - for k := range slv.(map[string]interface{}) { - if !verbose { - if k == "box_pub_key" || k == "box_sig_key" || k == "nodeinfo" || k == "was_mtu_fixed" { - continue - } - } - keyOrder = append(keyOrder, fmt.Sprint(k)) - } - sort.Strings(keyOrder) - keysOrdered = true + switch strings.ToLower(send.Name) { + case "list": + var resp admin.ListResponse + if err := json.Unmarshal(recv.Response, &resp); err != nil { + panic(err) + } + table.SetHeader([]string{"Command", "Arguments", "Description"}) + for _, entry := range resp.List { + for i := range entry.Fields { + entry.Fields[i] = entry.Fields[i] + "=..." } - for k, v := range slv.(map[string]interface{}) { - if len(fmt.Sprint(slk)) > maxWidths["key"] { - maxWidths["key"] = len(fmt.Sprint(slk)) - } - if len(fmt.Sprint(v)) > maxWidths[k] { - maxWidths[k] = len(fmt.Sprint(v)) - if maxWidths[k] < len(k) { - maxWidths[k] = len(k) - } - } + table.Append([]string{entry.Command, strings.Join(entry.Fields, ", "), entry.Description}) + } + table.Render() + + case "getself": + var resp admin.GetSelfResponse + if err := json.Unmarshal(recv.Response, &resp); err != nil { + panic(err) + } + table.Append([]string{"Build name:", resp.BuildName}) + table.Append([]string{"Build version:", resp.BuildVersion}) + table.Append([]string{"IPv6 address:", resp.IPAddress}) + table.Append([]string{"IPv6 subnet:", resp.Subnet}) + table.Append([]string{"Routing table size:", fmt.Sprintf("%d", resp.RoutingEntries)}) + table.Append([]string{"Public key:", resp.PublicKey}) + table.Render() + + case "getpeers": + var resp admin.GetPeersResponse + if err := json.Unmarshal(recv.Response, &resp); err != nil { + panic(err) + } + table.SetHeader([]string{"URI", "State", "Dir", "IP Address", "Uptime", "RTT", "RX", "TX", "Down", "Up", "Pr", "Cost", "Last Error"}) + for _, peer := range resp.Peers { + state, lasterr, dir, rtt, rxr, txr := "Up", "-", "Out", "-", "-", "-" + if !peer.Up { + state, lasterr = "Down", fmt.Sprintf("%s ago: %s", peer.LastErrorTime.Round(time.Second), peer.LastError) + } else if rttms := float64(peer.Latency.Microseconds()) / 1000; rttms > 0 { + rtt = fmt.Sprintf("%.02fms", rttms) } - } - - if len(keyOrder) > 0 { - fmt.Printf("%-"+fmt.Sprint(maxWidths["key"])+"s ", "") - for _, v := range keyOrder { - fmt.Printf("%-"+fmt.Sprint(maxWidths[v])+"s ", v) + if peer.Inbound { + dir = "In" } - fmt.Println() - } - - for slk, slv := range tlv.(map[string]interface{}) { - fmt.Printf("%-"+fmt.Sprint(maxWidths["key"])+"s ", slk) - for _, k := range keyOrder { - preformatted := slv.(map[string]interface{})[k] - var formatted string - switch k { - case "bytes_sent", "bytes_recvd": - formatted = fmt.Sprintf("%d", uint(preformatted.(float64))) - case "uptime", "last_seen": - seconds := uint(preformatted.(float64)) % 60 - minutes := uint(preformatted.(float64)/60) % 60 - hours := uint(preformatted.(float64) / 60 / 60) - formatted = fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds) - default: - formatted = fmt.Sprint(preformatted) - } - fmt.Printf("%-"+fmt.Sprint(maxWidths[k])+"s ", formatted) + uristring := peer.URI + if uri, err := url.Parse(peer.URI); err == nil { + uri.RawQuery = "" + uristring = uri.String() } - fmt.Println() - } - } -} - -func handleGetAndSetTunTap(res map[string]interface{}) { - for k, v := range res { - fmt.Println("Interface name:", k) - if mtu, ok := v.(map[string]interface{})["mtu"].(float64); ok { - fmt.Println("Interface MTU:", mtu) - } - if tap_mode, ok := v.(map[string]interface{})["tap_mode"].(bool); ok { - fmt.Println("TAP mode:", tap_mode) - } - } -} - -func handleGetSelf(res map[string]interface{}, verbose bool) { - for k, v := range res["self"].(map[string]interface{}) { - if buildname, ok := v.(map[string]interface{})["build_name"].(string); ok && buildname != "unknown" { - fmt.Println("Build name:", buildname) - } - if buildversion, ok := v.(map[string]interface{})["build_version"].(string); ok && buildversion != "unknown" { - fmt.Println("Build version:", buildversion) - } - fmt.Println("IPv6 address:", k) - if subnet, ok := v.(map[string]interface{})["subnet"].(string); ok { - fmt.Println("IPv6 subnet:", subnet) - } - if boxSigKey, ok := v.(map[string]interface{})["key"].(string); ok { - fmt.Println("Public key:", boxSigKey) - } - if coords, ok := v.(map[string]interface{})["coords"].([]interface{}); ok { - fmt.Println("Coords:", coords) - } - if verbose { - if nodeID, ok := v.(map[string]interface{})["node_id"].(string); ok { - fmt.Println("Node ID:", nodeID) + if peer.RXRate > 0 { + rxr = peer.RXRate.String() + "/s" } - if boxPubKey, ok := v.(map[string]interface{})["box_pub_key"].(string); ok { - fmt.Println("Public encryption key:", boxPubKey) + if peer.TXRate > 0 { + txr = peer.TXRate.String() + "/s" } - if boxSigKey, ok := v.(map[string]interface{})["box_sig_key"].(string); ok { - fmt.Println("Public signing key:", boxSigKey) + table.Append([]string{ + uristring, + state, + dir, + peer.IPAddress, + (time.Duration(peer.Uptime) * time.Second).String(), + rtt, + peer.RXBytes.String(), + peer.TXBytes.String(), + rxr, + txr, + fmt.Sprintf("%d", peer.Priority), + fmt.Sprintf("%d", peer.Cost), + lasterr, + }) + } + table.Render() + + case "gettree": + var resp admin.GetTreeResponse + if err := json.Unmarshal(recv.Response, &resp); err != nil { + panic(err) + } + //table.SetHeader([]string{"Public Key", "IP Address", "Port", "Rest"}) + table.SetHeader([]string{"Public Key", "IP Address", "Parent", "Sequence"}) + for _, tree := range resp.Tree { + table.Append([]string{ + tree.PublicKey, + tree.IPAddress, + tree.Parent, + fmt.Sprintf("%d", tree.Sequence), + //fmt.Sprintf("%d", dht.Port), + //fmt.Sprintf("%d", dht.Rest), + }) + } + table.Render() + + case "getpaths": + var resp admin.GetPathsResponse + if err := json.Unmarshal(recv.Response, &resp); err != nil { + panic(err) + } + table.SetHeader([]string{"Public Key", "IP Address", "Path", "Seq"}) + for _, p := range resp.Paths { + table.Append([]string{ + p.PublicKey, + p.IPAddress, + fmt.Sprintf("%v", p.Path), + fmt.Sprintf("%d", p.Sequence), + }) + } + table.Render() + + case "getsessions": + var resp admin.GetSessionsResponse + if err := json.Unmarshal(recv.Response, &resp); err != nil { + panic(err) + } + table.SetHeader([]string{"Public Key", "IP Address", "Uptime", "RX", "TX"}) + for _, p := range resp.Sessions { + table.Append([]string{ + p.PublicKey, + p.IPAddress, + (time.Duration(p.Uptime) * time.Second).String(), + p.RXBytes.String(), + p.TXBytes.String(), + }) + } + table.Render() + + case "getnodeinfo": + var resp core.GetNodeInfoResponse + if err := json.Unmarshal(recv.Response, &resp); err != nil { + panic(err) + } + for _, v := range resp { + fmt.Println(string(v)) + break + } + + case "getmulticastinterfaces": + var resp multicast.GetMulticastInterfacesResponse + if err := json.Unmarshal(recv.Response, &resp); err != nil { + panic(err) + } + fmtBool := func(b bool) string { + if b { + return "Yes" } + return "-" } - } -} + table.SetHeader([]string{"Name", "Listen Address", "Beacon", "Listen", "Password"}) + for _, p := range resp.Interfaces { + table.Append([]string{ + p.Name, + p.Address, + fmtBool(p.Beacon), + fmtBool(p.Listen), + fmtBool(p.Password), + }) + } + table.Render() -func handleGetSwitchQueues(res map[string]interface{}) { - maximumqueuesize := float64(4194304) - portqueues := make(map[float64]float64) - portqueuesize := make(map[float64]float64) - portqueuepackets := make(map[float64]float64) - v := res["switchqueues"].(map[string]interface{}) - if queuecount, ok := v["queues_count"].(float64); ok { - fmt.Printf("Active queue count: %d queues\n", uint(queuecount)) - } - if queuesize, ok := v["queues_size"].(float64); ok { - fmt.Printf("Active queue size: %d bytes\n", uint(queuesize)) - } - if highestqueuecount, ok := v["highest_queues_count"].(float64); ok { - fmt.Printf("Highest queue count: %d queues\n", uint(highestqueuecount)) - } - if highestqueuesize, ok := v["highest_queues_size"].(float64); ok { - fmt.Printf("Highest queue size: %d bytes\n", uint(highestqueuesize)) - } - if m, ok := v["maximum_queues_size"].(float64); ok { - maximumqueuesize = m - fmt.Printf("Maximum queue size: %d bytes\n", uint(maximumqueuesize)) - } - if queues, ok := v["queues"].([]interface{}); ok { - if len(queues) != 0 { - fmt.Println("Active queues:") - for _, v := range queues { - queueport := v.(map[string]interface{})["queue_port"].(float64) - queuesize := v.(map[string]interface{})["queue_size"].(float64) - queuepackets := v.(map[string]interface{})["queue_packets"].(float64) - queueid := v.(map[string]interface{})["queue_id"].(string) - portqueues[queueport]++ - portqueuesize[queueport] += queuesize - portqueuepackets[queueport] += queuepackets - queuesizepercent := (100 / maximumqueuesize) * queuesize - fmt.Printf("- Switch port %d, Stream ID: %v, size: %d bytes (%d%% full), %d packets\n", - uint(queueport), []byte(queueid), uint(queuesize), - uint(queuesizepercent), uint(queuepackets)) - } + case "gettun": + var resp tun.GetTUNResponse + if err := json.Unmarshal(recv.Response, &resp); err != nil { + panic(err) } - } - if len(portqueuesize) > 0 && len(portqueuepackets) > 0 { - fmt.Println("Aggregated statistics by switchport:") - for k, v := range portqueuesize { - queuesizepercent := (100 / (portqueues[k] * maximumqueuesize)) * v - fmt.Printf("- Switch port %d, size: %d bytes (%d%% full), %d packets\n", - uint(k), uint(v), uint(queuesizepercent), uint(portqueuepackets[k])) + table.Append([]string{"TUN enabled:", fmt.Sprintf("%#v", resp.Enabled)}) + if resp.Enabled { + table.Append([]string{"Interface name:", resp.Name}) + table.Append([]string{"Interface MTU:", fmt.Sprintf("%d", resp.MTU)}) } - } -} + table.Render() -func handleAddsAndRemoves(res map[string]interface{}) { - if _, ok := res["added"]; ok { - for _, v := range res["added"].([]interface{}) { - fmt.Println("Added:", fmt.Sprint(v)) - } - } - if _, ok := res["not_added"]; ok { - for _, v := range res["not_added"].([]interface{}) { - fmt.Println("Not added:", fmt.Sprint(v)) - } - } - if _, ok := res["removed"]; ok { - for _, v := range res["removed"].([]interface{}) { - fmt.Println("Removed:", fmt.Sprint(v)) - } - } - if _, ok := res["not_removed"]; ok { - for _, v := range res["not_removed"].([]interface{}) { - fmt.Println("Not removed:", fmt.Sprint(v)) - } - } -} + case "addpeer", "removepeer": -func handleGetAllowedEncryptionPublicKeys(res map[string]interface{}) { - if _, ok := res["allowed_box_pubs"]; !ok { - fmt.Println("All connections are allowed") - } else if res["allowed_box_pubs"] == nil { - fmt.Println("All connections are allowed") - } else { - fmt.Println("Connections are allowed only from the following public box keys:") - for _, v := range res["allowed_box_pubs"].([]interface{}) { - fmt.Println("-", v) - } + default: + fmt.Println(string(recv.Response)) } -} -func handleGetMulticastInterfaces(res map[string]interface{}) { - if _, ok := res["multicast_interfaces"]; !ok { - fmt.Println("No multicast interfaces found") - } else if res["multicast_interfaces"] == nil { - fmt.Println("No multicast interfaces found") - } else { - fmt.Println("Multicast peer discovery is active on:") - for _, v := range res["multicast_interfaces"].([]interface{}) { - fmt.Println("-", v) - } - } -} - -func handleGetSourceSubnets(res map[string]interface{}) { - if _, ok := res["source_subnets"]; !ok { - fmt.Println("No source subnets found") - } else if res["source_subnets"] == nil { - fmt.Println("No source subnets found") - } else { - fmt.Println("Source subnets:") - for _, v := range res["source_subnets"].([]interface{}) { - fmt.Println("-", v) - } - } -} - -func handleGetRoutes(res map[string]interface{}) { - if routes, ok := res["routes"].(map[string]interface{}); !ok { - fmt.Println("No routes found") - } else { - if res["routes"] == nil || len(routes) == 0 { - fmt.Println("No routes found") - } else { - fmt.Println("Routes:") - for k, v := range routes { - if pv, ok := v.(string); ok { - fmt.Println("-", k, " via ", pv) - } - } - } - } -} - -func handleGetTunnelRouting(res map[string]interface{}) { - if enabled, ok := res["enabled"].(bool); !ok { - fmt.Println("Tunnel routing is disabled") - } else if !enabled { - fmt.Println("Tunnel routing is disabled") - } else { - fmt.Println("Tunnel routing is enabled") - } + return 0 } diff --git a/contrib/.DS_Store b/contrib/.DS_Store new file mode 100644 index 00000000..61161474 Binary files /dev/null and b/contrib/.DS_Store differ diff --git a/contrib/ansible/genkeys.go b/contrib/ansible/genkeys.go index 4a02b9bd..c1aed7ee 100644 --- a/contrib/ansible/genkeys.go +++ b/contrib/ansible/genkeys.go @@ -1,7 +1,5 @@ /* - This file generates crypto keys for [ansible-yggdrasil](https://github.com/jcgruenhage/ansible-yggdrasil/) - */ package main diff --git a/contrib/apparmor/usr.bin.yggdrasilctl b/contrib/apparmor/usr.bin.yggdrasilctl new file mode 100644 index 00000000..2f2c8366 --- /dev/null +++ b/contrib/apparmor/usr.bin.yggdrasilctl @@ -0,0 +1,11 @@ +# Last Modified: Mon Feb 3 22:19:45 2025 +include + +/usr/bin/yggdrasilctl { + include + + /etc/yggdrasil.conf rw, + /run/yggdrasil.sock rw, + owner /sys/kernel/mm/transparent_hugepage/hpage_pmd_size r, + +} diff --git a/contrib/deb/generate.sh b/contrib/deb/generate.sh index ebe2753a..5731827c 100644 --- a/contrib/deb/generate.sh +++ b/contrib/deb/generate.sh @@ -21,13 +21,16 @@ if [ $PKGBRANCH = "master" ]; then PKGREPLACES=yggdrasil-develop fi -if [ $PKGARCH = "amd64" ]; then GOARCH=amd64 GOOS=linux ./build -elif [ $PKGARCH = "i386" ]; then GOARCH=386 GOOS=linux ./build -elif [ $PKGARCH = "mipsel" ]; then GOARCH=mipsle GOOS=linux ./build -elif [ $PKGARCH = "mips" ]; then GOARCH=mips64 GOOS=linux ./build -elif [ $PKGARCH = "armhf" ]; then GOARCH=arm GOOS=linux GOARM=6 ./build -elif [ $PKGARCH = "arm64" ]; then GOARCH=arm64 GOOS=linux ./build -elif [ $PKGARCH = "armel" ]; then GOARCH=arm GOOS=linux GOARM=5 ./build +GOLDFLAGS="-X github.com/yggdrasil-network/yggdrasil-go/src/config.defaultConfig=/etc/yggdrasil/yggdrasil.conf" +GOLDFLAGS="${GOLDFLAGS} -X github.com/yggdrasil-network/yggdrasil-go/src/config.defaultAdminListen=unix:///var/run/yggdrasil/yggdrasil.sock" + +if [ $PKGARCH = "amd64" ]; then GOARCH=amd64 GOOS=linux ./build -l "${GOLDFLAGS}" +elif [ $PKGARCH = "i386" ]; then GOARCH=386 GOOS=linux ./build -l "${GOLDFLAGS}" +elif [ $PKGARCH = "mipsel" ]; then GOARCH=mipsle GOOS=linux ./build -l "${GOLDFLAGS}" +elif [ $PKGARCH = "mips" ]; then GOARCH=mips64 GOOS=linux ./build -l "${GOLDFLAGS}" +elif [ $PKGARCH = "armhf" ]; then GOARCH=arm GOOS=linux GOARM=6 ./build -l "${GOLDFLAGS}" +elif [ $PKGARCH = "arm64" ]; then GOARCH=arm64 GOOS=linux ./build -l "${GOLDFLAGS}" +elif [ $PKGARCH = "armel" ]; then GOARCH=arm GOOS=linux GOARM=5 ./build -l "${GOLDFLAGS}" else echo "Specify PKGARCH=amd64,i386,mips,mipsel,armhf,arm64,armel" exit 1 @@ -38,7 +41,7 @@ echo "Building $PKGFILE" mkdir -p /tmp/$PKGNAME/ mkdir -p /tmp/$PKGNAME/debian/ mkdir -p /tmp/$PKGNAME/usr/bin/ -mkdir -p /tmp/$PKGNAME/etc/systemd/system/ +mkdir -p /tmp/$PKGNAME/lib/systemd/system/ cat > /tmp/$PKGNAME/debian/changelog << EOF Please see https://github.com/yggdrasil-network/yggdrasil-go/ @@ -47,11 +50,12 @@ echo 9 > /tmp/$PKGNAME/debian/compat cat > /tmp/$PKGNAME/debian/control << EOF Package: $PKGNAME Version: $PKGVERSION -Section: contrib/net -Priority: extra +Section: golang +Priority: optional Architecture: $PKGARCH Replaces: $PKGREPLACES Conflicts: $PKGREPLACES +Depends: systemd Maintainer: Neil Alexander Description: Yggdrasil Network Yggdrasil is an early-stage implementation of a fully end-to-end encrypted IPv6 @@ -68,35 +72,52 @@ EOF cat > /tmp/$PKGNAME/debian/install << EOF usr/bin/yggdrasil usr/bin usr/bin/yggdrasilctl usr/bin -etc/systemd/system/*.service etc/systemd/system +lib/systemd/system/*.service lib/systemd/system EOF cat > /tmp/$PKGNAME/debian/postinst << EOF #!/bin/sh +systemctl daemon-reload + if ! getent group yggdrasil 2>&1 > /dev/null; then - groupadd --system --force yggdrasil || echo "Failed to create group 'yggdrasil' - please create it manually and reinstall" + groupadd --system --force yggdrasil fi -if [ -f /etc/yggdrasil.conf ]; +if [ ! -d /etc/yggdrasil ]; +then + mkdir -p /etc/yggdrasil + chown root:yggdrasil /etc/yggdrasil + chmod 750 /etc/yggdrasil +fi + +if [ ! -f /etc/yggdrasil/yggdrasil.conf ]; +then + test -f /etc/yggdrasil.conf && mv /etc/yggdrasil.conf /etc/yggdrasil/yggdrasil.conf +fi + +if [ -f /etc/yggdrasil/yggdrasil.conf ]; then mkdir -p /var/backups echo "Backing up configuration file to /var/backups/yggdrasil.conf.`date +%Y%m%d`" - cp /etc/yggdrasil.conf /var/backups/yggdrasil.conf.`date +%Y%m%d` - echo "Normalising and updating /etc/yggdrasil.conf" - /usr/bin/yggdrasil -useconf -normaliseconf < /var/backups/yggdrasil.conf.`date +%Y%m%d` > /etc/yggdrasil.conf - chgrp yggdrasil /etc/yggdrasil.conf + cp /etc/yggdrasil/yggdrasil.conf /var/backups/yggdrasil.conf.`date +%Y%m%d` - if command -v systemctl >/dev/null; then - systemctl daemon-reload >/dev/null || true - systemctl enable yggdrasil || true - systemctl start yggdrasil || true - fi + echo "Normalising and updating /etc/yggdrasil/yggdrasil.conf" + /usr/bin/yggdrasil -useconf -normaliseconf < /var/backups/yggdrasil.conf.`date +%Y%m%d` > /etc/yggdrasil/yggdrasil.conf + + chown root:yggdrasil /etc/yggdrasil/yggdrasil.conf + chmod 640 /etc/yggdrasil/yggdrasil.conf else - echo "Generating initial configuration file /etc/yggdrasil.conf" - echo "Please familiarise yourself with this file before starting Yggdrasil" - sh -c 'umask 0027 && /usr/bin/yggdrasil -genconf > /etc/yggdrasil.conf' - chgrp yggdrasil /etc/yggdrasil.conf + echo "Generating initial configuration file /etc/yggdrasil/yggdrasil.conf" + /usr/bin/yggdrasil -genconf > /etc/yggdrasil/yggdrasil.conf + + chown root:yggdrasil /etc/yggdrasil/yggdrasil.conf + chmod 640 /etc/yggdrasil/yggdrasil.conf fi + +systemctl enable yggdrasil +systemctl restart yggdrasil + +exit 0 EOF cat > /tmp/$PKGNAME/debian/prerm << EOF #!/bin/sh @@ -110,13 +131,14 @@ EOF cp yggdrasil /tmp/$PKGNAME/usr/bin/ cp yggdrasilctl /tmp/$PKGNAME/usr/bin/ -cp contrib/systemd/*.service /tmp/$PKGNAME/etc/systemd/system/ +cp contrib/systemd/yggdrasil-default-config.service.debian /tmp/$PKGNAME/lib/systemd/system/yggdrasil-default-config.service +cp contrib/systemd/yggdrasil.service.debian /tmp/$PKGNAME/lib/systemd/system/yggdrasil.service -tar -czvf /tmp/$PKGNAME/data.tar.gz -C /tmp/$PKGNAME/ \ +tar --no-xattrs -czvf /tmp/$PKGNAME/data.tar.gz -C /tmp/$PKGNAME/ \ usr/bin/yggdrasil usr/bin/yggdrasilctl \ - etc/systemd/system/yggdrasil.service \ - etc/systemd/system/yggdrasil-default-config.service -tar -czvf /tmp/$PKGNAME/control.tar.gz -C /tmp/$PKGNAME/debian . + lib/systemd/system/yggdrasil.service \ + lib/systemd/system/yggdrasil-default-config.service +tar --no-xattrs -czvf /tmp/$PKGNAME/control.tar.gz -C /tmp/$PKGNAME/debian . echo 2.0 > /tmp/$PKGNAME/debian-binary ar -r $PKGFILE \ diff --git a/contrib/mobile/build b/contrib/mobile/build index 3c7b1d11..9be9529b 100755 --- a/contrib/mobile/build +++ b/contrib/mobile/build @@ -7,6 +7,7 @@ set -ef PKGSRC=${PKGSRC:-github.com/yggdrasil-network/yggdrasil-go/src/version} PKGNAME=${PKGNAME:-$(sh contrib/semver/name.sh)} PKGVER=${PKGVER:-$(sh contrib/semver/version.sh --bare)} +GOVER=$(go version | { read _ _ version _; echo ${version#go}; }) LDFLAGS="-X $PKGSRC.buildName=$PKGNAME -X $PKGSRC.buildVersion=$PKGVER" ARGS="-v" @@ -33,11 +34,20 @@ if [ ! $IOS ] && [ ! $ANDROID ]; then exit 1 fi +ver_le() { + printf "$1\n$2\n" | sort -VC +} + +if [ $ANDROID ] && ver_le 1.23.0 $GOVER ; then + # github.com/wlynxg/anet library relies on //go:linkname + LDFLAGS="$LDFLAGS -checklinkname=0" +fi + if [ $IOS ]; then echo "Building framework for iOS" go get golang.org/x/mobile/bind gomobile bind \ - -target ios -tags mobile -o Yggdrasil.xcframework \ + -target ios,macos -tags mobile -o Yggdrasil.xcframework \ -ldflags="$LDFLAGS $STRIP" -gcflags="$GCFLAGS" \ ./contrib/mobile ./src/config; fi diff --git a/contrib/mobile/mobile.go b/contrib/mobile/mobile.go index ba7cfdf6..72ea7d68 100644 --- a/contrib/mobile/mobile.go +++ b/contrib/mobile/mobile.go @@ -1,22 +1,21 @@ package mobile import ( + "crypto/ed25519" "encoding/hex" "encoding/json" - "fmt" "net" + "regexp" "github.com/gologme/log" "github.com/yggdrasil-network/yggdrasil-go/src/address" "github.com/yggdrasil-network/yggdrasil-go/src/config" "github.com/yggdrasil-network/yggdrasil-go/src/core" - "github.com/yggdrasil-network/yggdrasil-go/src/defaults" "github.com/yggdrasil-network/yggdrasil-go/src/ipv6rwc" "github.com/yggdrasil-network/yggdrasil-go/src/multicast" + "github.com/yggdrasil-network/yggdrasil-go/src/tun" "github.com/yggdrasil-network/yggdrasil-go/src/version" - - _ "golang.org/x/mobile/bind" ) // Yggdrasil mobile package is meant to "plug the gap" for mobile support, as @@ -25,11 +24,13 @@ import ( // functions. Note that in the case of iOS we handle reading/writing to/from TUN // in Swift therefore we use the "dummy" TUN interface instead. type Yggdrasil struct { - core core.Core + core *core.Core iprwc *ipv6rwc.ReadWriteCloser config *config.NodeConfig - multicast multicast.Multicast + multicast *multicast.Multicast + tun *tun.TunAdapter // optional log MobileLogger + logger *log.Logger } // StartAutoconfigure starts a node with a randomly generated config @@ -40,35 +41,85 @@ func (m *Yggdrasil) StartAutoconfigure() error { // StartJSON starts a node with the given JSON config. You can get JSON config // (rather than HJSON) by using the GenerateConfigJSON() function func (m *Yggdrasil) StartJSON(configjson []byte) error { + setMemLimitIfPossible() + logger := log.New(m.log, "", 0) logger.EnableLevel("error") logger.EnableLevel("warn") logger.EnableLevel("info") - m.config = defaults.GenerateConfig() - if err := json.Unmarshal(configjson, &m.config); err != nil { + m.logger = logger + m.config = config.GenerateConfig() + if err := m.config.UnmarshalHJSON(configjson); err != nil { return err } - m.config.IfName = "none" - if err := m.core.Start(m.config, logger); err != nil { - logger.Errorln("An error occured starting Yggdrasil:", err) - return err + // Set up the Yggdrasil node itself. + { + iprange := net.IPNet{ + IP: net.ParseIP("200::"), + Mask: net.CIDRMask(7, 128), + } + options := []core.SetupOption{ + core.PeerFilter(func(ip net.IP) bool { + return !iprange.Contains(ip) + }), + } + for _, peer := range m.config.Peers { + options = append(options, core.Peer{URI: peer}) + } + for intf, peers := range m.config.InterfacePeers { + for _, peer := range peers { + options = append(options, core.Peer{URI: peer, SourceInterface: intf}) + } + } + for _, allowed := range m.config.AllowedPublicKeys { + k, err := hex.DecodeString(allowed) + if err != nil { + panic(err) + } + options = append(options, core.AllowedPublicKey(k[:])) + } + for _, lAddr := range m.config.Listen { + options = append(options, core.ListenAddress(lAddr)) + } + var err error + m.core, err = core.New(m.config.Certificate, logger, options...) + if err != nil { + panic(err) + } + address, subnet := m.core.Address(), m.core.Subnet() + logger.Infof("Your public key is %s", hex.EncodeToString(m.core.PublicKey())) + logger.Infof("Your IPv6 address is %s", address.String()) + logger.Infof("Your IPv6 subnet is %s", subnet.String()) } + + // Set up the multicast module. + if len(m.config.MulticastInterfaces) > 0 { + var err error + logger.Infof("Initializing multicast %s", "") + options := []multicast.SetupOption{} + for _, intf := range m.config.MulticastInterfaces { + options = append(options, multicast.MulticastInterface{ + Regex: regexp.MustCompile(intf.Regex), + Beacon: intf.Beacon, + Listen: intf.Listen, + Port: intf.Port, + Priority: uint8(intf.Priority), + Password: intf.Password, + }) + } + logger.Infof("Starting multicast %s", "") + m.multicast, err = multicast.New(m.core, m.logger, options...) + if err != nil { + logger.Errorln("An error occurred starting multicast:", err) + } + } + mtu := m.config.IfMTU - m.iprwc = ipv6rwc.NewReadWriteCloser(&m.core) + m.iprwc = ipv6rwc.NewReadWriteCloser(m.core) if m.iprwc.MaxMTU() < mtu { mtu = m.iprwc.MaxMTU() } m.iprwc.SetMTU(mtu) - if len(m.config.MulticastInterfaces) > 0 { - if err := m.multicast.Init(&m.core, m.config, logger, nil); err != nil { - logger.Errorln("An error occurred initialising multicast:", err) - return err - } - if err := m.multicast.Start(); err != nil { - logger.Errorln("An error occurred starting multicast:", err) - return err - } - } return nil } @@ -82,6 +133,18 @@ func (m *Yggdrasil) Send(p []byte) error { return nil } +// Send sends a packet from given buffer to Yggdrasil. From first byte up to length. +func (m *Yggdrasil) SendBuffer(p []byte, length int) error { + if m.iprwc == nil { + return nil + } + if len(p) < length { + return nil + } + _, _ = m.iprwc.Write(p[:length]) + return nil +} + // Recv waits for and reads a packet coming from Yggdrasil. It // will be a fully formed IPv6 packet func (m *Yggdrasil) Recv() ([]byte, error) { @@ -93,21 +156,45 @@ func (m *Yggdrasil) Recv() ([]byte, error) { return buf[:n], nil } +// Recv waits for and reads a packet coming from Yggdrasil to given buffer, returning size of packet +func (m *Yggdrasil) RecvBuffer(buf []byte) (int, error) { + if m.iprwc == nil { + return 0, nil + } + n, _ := m.iprwc.Read(buf) + return n, nil +} + // Stop the mobile Yggdrasil instance func (m *Yggdrasil) Stop() error { logger := log.New(m.log, "", 0) logger.EnableLevel("info") - logger.Infof("Stop the mobile Yggdrasil instance %s", "") - if err := m.multicast.Stop(); err != nil { - return err + logger.Infof("Stopping the mobile Yggdrasil instance %s", "") + if m.multicast != nil { + logger.Infof("Stopping multicast %s", "") + if err := m.multicast.Stop(); err != nil { + return err + } } + logger.Infof("Stopping TUN device %s", "") + if m.tun != nil { + if err := m.tun.Stop(); err != nil { + return err + } + } + logger.Infof("Stopping Yggdrasil core %s", "") m.core.Stop() return nil } +// Retry resets the peer connection timer and tries to dial them immediately. +func (m *Yggdrasil) RetryPeersNow() { + m.core.RetryPeersNow() +} + // GenerateConfigJSON generates mobile-friendly configuration in JSON format func GenerateConfigJSON() []byte { - nc := defaults.GenerateConfig() + nc := config.GenerateConfig() nc.IfName = "none" if json, err := json.Marshal(nc); err == nil { return json @@ -132,25 +219,28 @@ func (m *Yggdrasil) GetPublicKeyString() string { return hex.EncodeToString(m.core.GetSelf().Key) } -// GetCoordsString gets the node's coordinates -func (m *Yggdrasil) GetCoordsString() string { - return fmt.Sprintf("%v", m.core.GetSelf().Coords) +// GetRoutingEntries gets the number of entries in the routing table +func (m *Yggdrasil) GetRoutingEntries() int { + return int(m.core.GetSelf().RoutingEntries) } func (m *Yggdrasil) GetPeersJSON() (result string) { peers := []struct { - core.Peer + core.PeerInfo IP string }{} for _, v := range m.core.GetPeers() { - a := address.AddrForKey(v.Key) - ip := net.IP(a[:]).String() + var ip string + if v.Key != nil { + a := address.AddrForKey(v.Key) + ip = net.IP(a[:]).String() + } peers = append(peers, struct { - core.Peer + core.PeerInfo IP string }{ - Peer: v, - IP: ip, + PeerInfo: v, + IP: ip, }) } if res, err := json.Marshal(peers); err == nil { @@ -160,8 +250,16 @@ func (m *Yggdrasil) GetPeersJSON() (result string) { } } -func (m *Yggdrasil) GetDHTJSON() (result string) { - if res, err := json.Marshal(m.core.GetDHT()); err == nil { +func (m *Yggdrasil) GetPathsJSON() (result string) { + if res, err := json.Marshal(m.core.GetPaths()); err == nil { + return string(res) + } else { + return "{}" + } +} + +func (m *Yggdrasil) GetTreeJSON() (result string) { + if res, err := json.Marshal(m.core.GetTree()); err == nil { return string(res) } else { return "{}" @@ -176,3 +274,28 @@ func (m *Yggdrasil) GetMTU() int { func GetVersion() string { return version.BuildVersion() } + +type ConfigSummary struct { + PublicKey string + IPv6Address string + IPv6Subnet string +} + +func SummaryForConfig(b []byte) *ConfigSummary { + cfg := config.GenerateConfig() + if err := cfg.UnmarshalHJSON(b); err != nil { + return nil + } + pub := ed25519.PrivateKey(cfg.PrivateKey).Public().(ed25519.PublicKey) + hpub := hex.EncodeToString(pub) + addr := net.IP(address.AddrForKey(pub)[:]) + snet := net.IPNet{ + IP: append(address.SubnetForKey(pub)[:], 0, 0, 0, 0, 0, 0, 0, 0), + Mask: net.CIDRMask(64, 128), + } + return &ConfigSummary{ + PublicKey: hpub, + IPv6Address: addr.String(), + IPv6Subnet: snet.String(), + } +} diff --git a/contrib/mobile/mobile_ios.go b/contrib/mobile/mobile_apple.go similarity index 57% rename from contrib/mobile/mobile_ios.go rename to contrib/mobile/mobile_apple.go index fedee2db..2c53fa94 100644 --- a/contrib/mobile/mobile_ios.go +++ b/contrib/mobile/mobile_apple.go @@ -1,5 +1,5 @@ -//go:build ios -// +build ios +//go:build ios || darwin +// +build ios darwin package mobile @@ -15,6 +15,8 @@ void Log(const char *text) { import "C" import ( "unsafe" + + "github.com/yggdrasil-network/yggdrasil-go/src/tun" ) type MobileLogger struct { @@ -26,3 +28,13 @@ func (nsl MobileLogger) Write(p []byte) (n int, err error) { C.Log(cstr) return len(p), nil } + +func (m *Yggdrasil) TakeOverTUN(fd int32) error { + options := []tun.SetupOption{ + tun.FileDescriptor(fd), + tun.InterfaceMTU(m.iprwc.MTU()), + } + var err error + m.tun, err = tun.New(m.iprwc, m.logger, options...) + return err +} diff --git a/contrib/mobile/mobile_mem_go120.go b/contrib/mobile/mobile_mem_go120.go new file mode 100644 index 00000000..853f1aba --- /dev/null +++ b/contrib/mobile/mobile_mem_go120.go @@ -0,0 +1,10 @@ +//go:build go1.20 +// +build go1.20 + +package mobile + +import "runtime/debug" + +func setMemLimitIfPossible() { + debug.SetMemoryLimit(1024 * 1024 * 40) +} diff --git a/contrib/mobile/mobile_mem_other.go b/contrib/mobile/mobile_mem_other.go new file mode 100644 index 00000000..729d9c2a --- /dev/null +++ b/contrib/mobile/mobile_mem_other.go @@ -0,0 +1,8 @@ +//go:build !go1.20 +// +build !go1.20 + +package mobile + +func setMemLimitIfPossible() { + // not supported by this Go version +} diff --git a/contrib/mobile/mobile_other.go b/contrib/mobile/mobile_other.go index aceeb7e2..10804b3e 100644 --- a/contrib/mobile/mobile_other.go +++ b/contrib/mobile/mobile_other.go @@ -1,5 +1,5 @@ -//go:build !android && !ios -// +build !android,!ios +//go:build !android && !ios && !darwin +// +build !android,!ios,!darwin package mobile diff --git a/contrib/mobile/mobile_test.go b/contrib/mobile/mobile_test.go index 19916407..74689294 100644 --- a/contrib/mobile/mobile_test.go +++ b/contrib/mobile/mobile_test.go @@ -1,15 +1,27 @@ package mobile -import "testing" +import ( + "os" + "testing" + + "github.com/gologme/log" +) func TestStartYggdrasil(t *testing.T) { - ygg := &Yggdrasil{} + logger := log.New(os.Stdout, "", 0) + logger.EnableLevel("error") + logger.EnableLevel("warn") + logger.EnableLevel("info") + + ygg := &Yggdrasil{ + logger: logger, + } if err := ygg.StartAutoconfigure(); err != nil { t.Fatalf("Failed to start Yggdrasil: %s", err) } t.Log("Address:", ygg.GetAddressString()) t.Log("Subnet:", ygg.GetSubnetString()) - t.Log("Coords:", ygg.GetCoordsString()) + t.Log("Routing entries:", ygg.GetRoutingEntries()) if err := ygg.Stop(); err != nil { t.Fatalf("Failed to stop Yggdrasil: %s", err) } diff --git a/contrib/msi/build-msi.sh b/contrib/msi/build-msi.sh index 38b9b810..857aeec9 100644 --- a/contrib/msi/build-msi.sh +++ b/contrib/msi/build-msi.sh @@ -1,7 +1,7 @@ #!/bin/sh # This script generates an MSI file for Yggdrasil for a given architecture. It -# needs to run on Windows within MSYS2 and Go 1.17 or later must be installed on +# needs to run on Windows within MSYS2 and Go 1.21 or later must be installed on # the system and within the PATH. This is ran currently by GitHub Actions (see # the workflows in the repository). # @@ -16,20 +16,7 @@ then fi # Download the wix tools! -if [ ! -d wixbin ]; -then - curl -LO https://wixtoolset.org/downloads/v3.14.0.3910/wix314-binaries.zip - if [ `md5sum wix314-binaries.zip | cut -f 1 -d " "` != "34f655cf108086838dd5a76d4318063b" ]; - then - echo "wix package didn't match expected checksum" - exit 1 - fi - mkdir -p wixbin - unzip -o wix314-binaries.zip -d wixbin || ( - echo "failed to unzip WiX" - exit 1 - ) -fi +dotnet tool install --global wix --version 5.0.0 # Build Yggdrasil! [ "${PKGARCH}" == "x64" ] && GOOS=windows GOARCH=amd64 CGO_ENABLED=0 ./build @@ -61,6 +48,11 @@ PKGVERSIONMS=$(echo $PKGVERSION | tr - .) if [ ! -d wintun ]; then curl -o wintun.zip https://www.wintun.net/builds/wintun-0.14.1.zip + if [ `sha256sum wintun.zip | cut -f 1 -d " "` != "07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51" ]; + then + echo "wintun package didn't match expected checksum" + exit 1 + fi unzip wintun.zip fi if [ $PKGARCH = "x64" ]; then @@ -101,7 +93,7 @@ cat > wix.xml << EOF Description="Yggdrasil Network Installer" Comments="Yggdrasil Network standalone router for Windows." Manufacturer="github.com/yggdrasil-network" - InstallerVersion="200" + InstallerVersion="500" InstallScope="perMachine" Languages="1033" Compressed="yes" @@ -205,5 +197,5 @@ EOF # Generate the MSI CANDLEFLAGS="-nologo" LIGHTFLAGS="-nologo -spdb -sice:ICE71 -sice:ICE61" -wixbin/candle $CANDLEFLAGS -out ${PKGNAME}-${PKGVERSION}-${PKGARCH}.wixobj -arch ${PKGARCH} wix.xml && \ -wixbin/light $LIGHTFLAGS -ext WixUtilExtension.dll -out ${PKGNAME}-${PKGVERSION}-${PKGARCH}.msi ${PKGNAME}-${PKGVERSION}-${PKGARCH}.wixobj +candle $CANDLEFLAGS -out ${PKGNAME}-${PKGVERSION}-${PKGARCH}.wixobj -arch ${PKGARCH} wix.xml && \ +light $LIGHTFLAGS -ext WixUtilExtension.dll -out ${PKGNAME}-${PKGVERSION}-${PKGARCH}.msi ${PKGNAME}-${PKGVERSION}-${PKGARCH}.wixobj diff --git a/contrib/openrc/yggdrasil b/contrib/openrc/yggdrasil index 4a2e0a13..aece8ecb 100755 --- a/contrib/openrc/yggdrasil +++ b/contrib/openrc/yggdrasil @@ -6,7 +6,6 @@ CONFFILE="/etc/yggdrasil.conf" pidfile="/run/${RC_SVCNAME}.pid" command="/usr/bin/yggdrasil" -extra_started_commands="reload" depend() { use net dns logger @@ -42,12 +41,6 @@ start() { eend $? } -reload() { - ebegin "Reloading ${RC_SVCNAME}" - start-stop-daemon --signal HUP --pidfile "${pidfile}" - eend $? -} - stop() { ebegin "Stopping ${RC_SVCNAME}" start-stop-daemon --stop --pidfile "${pidfile}" --exec "${command}" diff --git a/contrib/systemd/yggdrasil-default-config.service.debian b/contrib/systemd/yggdrasil-default-config.service.debian new file mode 100644 index 00000000..dc3fdc5a --- /dev/null +++ b/contrib/systemd/yggdrasil-default-config.service.debian @@ -0,0 +1,13 @@ +[Unit] +Description=Yggdrasil default config generator +ConditionPathExists=|!/etc/yggdrasil/yggdrasil.conf +ConditionFileNotEmpty=|!/etc/yggdrasil/yggdrasil.conf +Wants=local-fs.target +After=local-fs.target + +[Service] +Type=oneshot +Group=yggdrasil +ExecStartPre=/usr/bin/mkdir -p /etc/yggdrasil +ExecStart=/usr/bin/yggdrasil -genconf > /etc/yggdrasil/yggdrasil.conf +ExecStartPost=/usr/bin/chmod -R 0640 /etc/yggdrasil diff --git a/contrib/systemd/yggdrasil.service.debian b/contrib/systemd/yggdrasil.service.debian new file mode 100644 index 00000000..0f3c7a8d --- /dev/null +++ b/contrib/systemd/yggdrasil.service.debian @@ -0,0 +1,25 @@ +[Unit] +Description=Yggdrasil Network +Wants=network-online.target +Wants=yggdrasil-default-config.service +After=network-online.target +After=yggdrasil-default-config.service + +[Service] +Group=yggdrasil +ProtectHome=true +ProtectSystem=strict +NoNewPrivileges=true +RuntimeDirectory=yggdrasil +ReadWritePaths=/var/run/yggdrasil/ /run/yggdrasil/ +SyslogIdentifier=yggdrasil +CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE +AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE +ExecStartPre=+-/sbin/modprobe tun +ExecStart=/usr/bin/yggdrasil -useconffile /etc/yggdrasil/yggdrasil.conf +ExecReload=/bin/kill -HUP $MAINPID +Restart=always +TimeoutStopSec=5 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/go.mod b/go.mod index c93b8a0b..672edd6e 100644 --- a/go.mod +++ b/go.mod @@ -1,35 +1,49 @@ module github.com/yggdrasil-network/yggdrasil-go -go 1.17 +go 1.22 require ( - github.com/Arceliar/ironwood v0.0.0-20220409035209-b7f71f05435a - github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979 - github.com/cheggaaa/pb/v3 v3.0.8 - github.com/gologme/log v1.2.0 + github.com/Arceliar/ironwood v0.0.0-20241213013129-743fe2fccbd3 + github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d + github.com/cheggaaa/pb/v3 v3.1.5 + github.com/coder/websocket v1.8.12 + github.com/gologme/log v1.3.0 github.com/hashicorp/go-syslog v1.0.0 - github.com/hjson/hjson-go v3.1.0+incompatible - github.com/kardianos/minwinsvc v1.0.0 - github.com/mitchellh/mapstructure v1.4.1 - github.com/vishvananda/netlink v1.1.0 - golang.org/x/mobile v0.0.0-20220112015953-858099ff7816 - golang.org/x/net v0.0.0-20211101193420-4a448f8816b3 - golang.org/x/sys v0.0.0-20211102192858-4dd72447c267 - golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b - golang.zx2c4.com/wireguard v0.0.0-20211017052713-f87e87af0d9a - golang.zx2c4.com/wireguard/windows v0.4.12 + github.com/hjson/hjson-go/v4 v4.4.0 + github.com/kardianos/minwinsvc v1.0.2 + github.com/quic-go/quic-go v0.48.2 + github.com/vishvananda/netlink v1.3.0 + github.com/wlynxg/anet v0.0.5 + golang.org/x/crypto v0.33.0 + golang.org/x/net v0.35.0 + golang.org/x/sys v0.30.0 + golang.org/x/text v0.22.0 + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 + golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 + golang.zx2c4.com/wireguard/windows v0.5.3 +) + +require ( + github.com/bits-and-blooms/bitset v1.13.0 // indirect + github.com/bits-and-blooms/bloom/v3 v3.7.0 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/onsi/ginkgo/v2 v2.9.5 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/mod v0.19.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/tools v0.23.0 // indirect ) require ( github.com/VividCortex/ewma v1.2.0 // indirect - github.com/fatih/color v1.12.0 // indirect - github.com/mattn/go-colorable v0.1.8 // indirect - github.com/mattn/go-isatty v0.0.13 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect - github.com/rivo/uniseg v0.2.0 // indirect - github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect - golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect - golang.org/x/mod v0.4.2 // indirect - golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098 // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/olekukonko/tablewriter v0.0.5 + github.com/vishvananda/netns v0.0.5 // indirect + suah.dev/protect v1.2.4 ) diff --git a/go.sum b/go.sum index c87112f7..d843af40 100644 --- a/go.sum +++ b/go.sum @@ -1,106 +1,119 @@ -github.com/Arceliar/ironwood v0.0.0-20220409035209-b7f71f05435a h1:yfbnOyqPcx2gi5cFIJ2rlPz5M6rFPHT/c8FgZmFjCdc= -github.com/Arceliar/ironwood v0.0.0-20220409035209-b7f71f05435a/go.mod h1:RP72rucOFm5udrnEzTmIWLRVGQiV/fSUAQXJ0RST/nk= -github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979 h1:WndgpSW13S32VLQ3ugUxx2EnnWmgba1kCqPkd4Gk1yQ= -github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979/go.mod h1:6Lkn+/zJilRMsKmbmG1RPoamiArC6HS73xbwRyp3UyI= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= +github.com/Arceliar/ironwood v0.0.0-20241213013129-743fe2fccbd3 h1:d8N0z+udAnbU5PdjpLSNPTWlqeU/nnYsQ42B6+879aw= +github.com/Arceliar/ironwood v0.0.0-20241213013129-743fe2fccbd3/go.mod h1:SrrElc3FFMpYCODSr11jWbLFeOM8WsY+DbDY/l2AXF0= +github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d h1:UK9fsWbWqwIQkMCz1CP+v5pGbsGoWAw6g4AyvMpm1EM= +github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d/go.mod h1:BCnxhRf47C/dy/e/D2pmB8NkB3dQVIrkD98b220rx5Q= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= -github.com/cheggaaa/pb/v3 v3.0.8 h1:bC8oemdChbke2FHIIGy9mn4DPJ2caZYQnfbRqwmdCoA= -github.com/cheggaaa/pb/v3 v3.0.8/go.mod h1:UICbiLec/XO6Hw6k+BHEtHeQFzzBH4i2/qk/ow1EJTA= -github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= -github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/gologme/log v1.2.0 h1:Ya5Ip/KD6FX7uH0S31QO87nCCSucKtF44TLbTtO7V4c= -github.com/gologme/log v1.2.0/go.mod h1:gq31gQ8wEHkR+WekdWsqDuf8pXTUZA9BnnzTuPz1Y9U= +github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= +github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bloom/v3 v3.7.0 h1:VfknkqV4xI+PsaDIsoHueyxVDZrfvMn56jeWUzvzdls= +github.com/bits-and-blooms/bloom/v3 v3.7.0/go.mod h1:VKlUSvp0lFIYqxJjzdnSsZEw4iHb1kOL2tfHTgyJBHg= +github.com/cheggaaa/pb/v3 v3.1.5 h1:QuuUzeM2WsAqG2gMqtzaWithDJv0i+i6UlnwSCI4QLk= +github.com/cheggaaa/pb/v3 v3.1.5/go.mod h1:CrxkeghYTXi1lQBEI7jSn+3svI3cuc19haAj6jM60XI= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/gologme/log v1.3.0 h1:l781G4dE+pbigClDSDzSaaYKtiueHCILUa/qSDsmHAo= +github.com/gologme/log v1.3.0/go.mod h1:yKT+DvIPdDdDoPtqFrFxheooyVmoqi0BAsw+erN3wA4= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hjson/hjson-go v3.1.0+incompatible h1:DY/9yE8ey8Zv22bY+mHV1uk2yRy0h8tKhZ77hEdi0Aw= -github.com/hjson/hjson-go v3.1.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio= -github.com/kardianos/minwinsvc v1.0.0 h1:+JfAi8IBJna0jY2dJGZqi7o15z13JelFIklJCAENALA= -github.com/kardianos/minwinsvc v1.0.0/go.mod h1:Bgd0oc+D0Qo3bBytmNtyRKVlp85dAloLKhfxanPFFRc= -github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= -github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= -github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= -github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/hjson/hjson-go/v4 v4.4.0 h1:D/NPvqOCH6/eisTb5/ztuIS8GUvmpHaLOcNk1Bjr298= +github.com/hjson/hjson-go/v4 v4.4.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/kardianos/minwinsvc v1.0.2 h1:JmZKFJQrmTGa/WiW+vkJXKmfzdjabuEW4Tirj5lLdR0= +github.com/kardianos/minwinsvc v1.0.2/go.mod h1:LUZNYhNmxujx2tR7FbdxqYJ9XDDoCd3MQcl1o//FWl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= +github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE= +github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= -github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= -github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= -github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f h1:p4VB7kIXpOQvVn1ZaTIVp+3vuYAXFe3OJEvjbUYJLaA= -github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20220112015953-858099ff7816 h1:jhDgkcu3yQ4tasBZ+1YwDmK7eFmuVf1w1k+NGGGxfmE= -golang.org/x/mobile v0.0.0-20220112015953-858099ff7816/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210927181540-4e4d966f7476/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211011170408-caeb26a5c8c0/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211101193420-4a448f8816b3 h1:VrJZAjbekhoRn7n5FBujY31gboH+iB3pdLxn3gE9FjU= -golang.org/x/net v0.0.0-20211101193420-4a448f8816b3/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211102192858-4dd72447c267 h1:7zYaz3tjChtpayGDzu6H0hDAUM5zIGA2XW7kRNgQ0jc= -golang.org/x/sys v0.0.0-20211102192858-4dd72447c267/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b h1:NXqSWXSRUSCaFuvitrWtU169I3876zRTalMRbfd6LL0= -golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b/go.mod h1:EFNZuWvGYxIRUEX+K8UmCFwYmZjqcrnq15ZuVldZkZ0= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098 h1:YuekqPskqwCCPM79F1X5Dhv4ezTCj+Ki1oNwiafxkA0= -golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.zx2c4.com/wireguard v0.0.0-20211012062646-82d2aa87aa62/go.mod h1:id8Oh3eCCmpj9uVGWVjsUAl6UPX5ysMLzu6QxJU2UOU= -golang.zx2c4.com/wireguard v0.0.0-20211017052713-f87e87af0d9a h1:tTbyylK9/D3u/wEP26Vx7L700UpY48nhioJWZM1vhZw= -golang.zx2c4.com/wireguard v0.0.0-20211017052713-f87e87af0d9a/go.mod h1:id8Oh3eCCmpj9uVGWVjsUAl6UPX5ysMLzu6QxJU2UOU= -golang.zx2c4.com/wireguard/windows v0.4.12 h1:CUmbdWKVNzTSsVb4yUAiEwL3KsabdJkEPdDjCHxBlhA= -golang.zx2c4.com/wireguard/windows v0.4.12/go.mod h1:PW4y+d9oY83XU9rRwRwrJDwEMuhVjMxu2gfD1cfzS7w= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg= +github.com/twmb/murmur3 v1.1.6/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= +github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= +github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= +golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= +golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= +golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= +gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= +suah.dev/protect v1.2.4 h1:iVZG/zQB63FKNpITDYM/cXoAeCTIjCiXHuFVByJFDzg= +suah.dev/protect v1.2.4/go.mod h1:vVrquYO3u1Ep9Ez2z8x+6N6/czm+TBmWKZfiXU2tb54= diff --git a/src/address/address.go b/src/address/address.go index d56be80d..c9581c70 100644 --- a/src/address/address.go +++ b/src/address/address.go @@ -113,7 +113,7 @@ func SubnetForKey(publicKey ed25519.PublicKey) *Subnet { return &snet } -// GetKet returns the partial ed25519.PublicKey for the Address. +// GetKey returns the partial ed25519.PublicKey for the Address. // This is used for key lookup. func (a *Address) GetKey() ed25519.PublicKey { var key [ed25519.PublicKeySize]byte @@ -141,7 +141,7 @@ func (a *Address) GetKey() ed25519.PublicKey { return ed25519.PublicKey(key[:]) } -// GetKet returns the partial ed25519.PublicKey for the Subnet. +// GetKey returns the partial ed25519.PublicKey for the Subnet. // This is used for key lookup. func (s *Subnet) GetKey() ed25519.PublicKey { var addr Address diff --git a/src/address/address_test.go b/src/address/address_test.go index a7939e0f..5aafd5a4 100644 --- a/src/address/address_test.go +++ b/src/address/address_test.go @@ -3,13 +3,13 @@ package address import ( "bytes" "crypto/ed25519" - "math/rand" + "crypto/rand" "testing" ) func TestAddress_Address_IsValid(t *testing.T) { var address Address - rand.Read(address[:]) + _, _ = rand.Read(address[:]) address[0] = 0 @@ -32,7 +32,7 @@ func TestAddress_Address_IsValid(t *testing.T) { func TestAddress_Subnet_IsValid(t *testing.T) { var subnet Subnet - rand.Read(subnet[:]) + _, _ = rand.Read(subnet[:]) subnet[0] = 0 diff --git a/src/admin/addpeer.go b/src/admin/addpeer.go new file mode 100644 index 00000000..a897e45e --- /dev/null +++ b/src/admin/addpeer.go @@ -0,0 +1,21 @@ +package admin + +import ( + "fmt" + "net/url" +) + +type AddPeerRequest struct { + Uri string `json:"uri"` + Sintf string `json:"interface,omitempty"` +} + +type AddPeerResponse struct{} + +func (a *AdminSocket) addPeerHandler(req *AddPeerRequest, _ *AddPeerResponse) error { + u, err := url.Parse(req.Uri) + if err != nil { + return fmt.Errorf("unable to parse peering URI: %w", err) + } + return a.core.AddPeer(u, req.Sintf) +} diff --git a/src/admin/admin.go b/src/admin/admin.go index 56164533..54c1a124 100644 --- a/src/admin/admin.go +++ b/src/admin/admin.go @@ -7,55 +7,63 @@ import ( "net" "net/url" "os" + "sort" "strings" "time" - "github.com/gologme/log" - - "github.com/yggdrasil-network/yggdrasil-go/src/config" "github.com/yggdrasil-network/yggdrasil-go/src/core" ) // TODO: Add authentication type AdminSocket struct { - core *core.Core - log *log.Logger - listenaddr string - listener net.Listener - handlers map[string]handler - done chan struct{} + core *core.Core + log core.Logger + listener net.Listener + handlers map[string]handler + done chan struct{} + config struct { + listenaddr ListenAddress + } +} + +type AdminSocketRequest struct { + Name string `json:"request"` + Arguments json.RawMessage `json:"arguments,omitempty"` + KeepAlive bool `json:"keepalive,omitempty"` } type AdminSocketResponse struct { - Status string `json:"status"` - Request struct { - Name string `json:"request"` - KeepAlive bool `json:"keepalive"` - } `json:"request"` - Response interface{} `json:"response"` + Status string `json:"status"` + Error string `json:"error,omitempty"` + Request AdminSocketRequest `json:"request"` + Response json.RawMessage `json:"response"` } type handler struct { + desc string // What does the endpoint do? args []string // List of human-readable argument names handler core.AddHandlerFunc // First is input map, second is output } type ListResponse struct { - List map[string]ListEntry `json:"list"` + List []ListEntry `json:"list"` } type ListEntry struct { - Fields []string `json:"fields"` + Command string `json:"command"` + Description string `json:"description"` + Fields []string `json:"fields,omitempty"` } // AddHandler is called for each admin function to add the handler and help documentation to the API. -func (a *AdminSocket) AddHandler(name string, args []string, handlerfunc core.AddHandlerFunc) error { +func (a *AdminSocket) AddHandler(name, desc string, args []string, handlerfunc core.AddHandlerFunc) error { if _, ok := a.handlers[strings.ToLower(name)]; ok { return errors.New("handler already exists") } a.handlers[strings.ToLower(name)] = handler{ + desc: desc, args: args, handler: handlerfunc, } @@ -63,98 +71,182 @@ func (a *AdminSocket) AddHandler(name string, args []string, handlerfunc core.Ad } // Init runs the initial admin setup. -func (a *AdminSocket) Init(c *core.Core, nc *config.NodeConfig, log *log.Logger, options interface{}) error { - a.core = c - a.log = log - a.handlers = make(map[string]handler) - nc.RLock() - a.listenaddr = nc.AdminListen - nc.RUnlock() - a.done = make(chan struct{}) - close(a.done) // Start in a done / not-started state - _ = a.AddHandler("list", []string{}, func(_ json.RawMessage) (interface{}, error) { - res := &ListResponse{ - List: map[string]ListEntry{}, - } - for name, handler := range a.handlers { - res.List[name] = ListEntry{ - Fields: handler.args, - } - } - return res, nil - }) - return a.core.SetAdmin(a) -} - -func (a *AdminSocket) SetupAdminHandlers(na *AdminSocket) { - _ = a.AddHandler("getSelf", []string{}, func(in json.RawMessage) (interface{}, error) { - req := &GetSelfRequest{} - res := &GetSelfResponse{} - if err := json.Unmarshal(in, &req); err != nil { - return nil, err - } - if err := a.getSelfHandler(req, res); err != nil { - return nil, err - } - return res, nil - }) - _ = a.AddHandler("getPeers", []string{}, func(in json.RawMessage) (interface{}, error) { - req := &GetPeersRequest{} - res := &GetPeersResponse{} - if err := json.Unmarshal(in, &req); err != nil { - return nil, err - } - if err := a.getPeersHandler(req, res); err != nil { - return nil, err - } - return res, nil - }) - _ = a.AddHandler("getDHT", []string{}, func(in json.RawMessage) (interface{}, error) { - req := &GetDHTRequest{} - res := &GetDHTResponse{} - if err := json.Unmarshal(in, &req); err != nil { - return nil, err - } - if err := a.getDHTHandler(req, res); err != nil { - return nil, err - } - return res, nil - }) - _ = a.AddHandler("getPaths", []string{}, func(in json.RawMessage) (interface{}, error) { - req := &GetPathsRequest{} - res := &GetPathsResponse{} - if err := json.Unmarshal(in, &req); err != nil { - return nil, err - } - if err := a.getPathsHandler(req, res); err != nil { - return nil, err - } - return res, nil - }) - _ = a.AddHandler("getSessions", []string{}, func(in json.RawMessage) (interface{}, error) { - req := &GetSessionsRequest{} - res := &GetSessionsResponse{} - if err := json.Unmarshal(in, &req); err != nil { - return nil, err - } - if err := a.getSessionsHandler(req, res); err != nil { - return nil, err - } - return res, nil - }) - //_ = a.AddHandler("getNodeInfo", []string{"key"}, t.proto.nodeinfo.nodeInfoAdminHandler) - //_ = a.AddHandler("debug_remoteGetSelf", []string{"key"}, t.proto.getSelfHandler) - //_ = a.AddHandler("debug_remoteGetPeers", []string{"key"}, t.proto.getPeersHandler) - //_ = a.AddHandler("debug_remoteGetDHT", []string{"key"}, t.proto.getDHTHandler) -} - -// Start runs the admin API socket to listen for / respond to admin API calls. -func (a *AdminSocket) Start() error { - if a.listenaddr != "none" && a.listenaddr != "" { - a.done = make(chan struct{}) - go a.listen() +func New(c *core.Core, log core.Logger, opts ...SetupOption) (*AdminSocket, error) { + a := &AdminSocket{ + core: c, + log: log, + handlers: make(map[string]handler), } - return nil + for _, opt := range opts { + a._applyOption(opt) + } + if a.config.listenaddr == "none" || a.config.listenaddr == "" { + return nil, nil + } + + listenaddr := string(a.config.listenaddr) + u, err := url.Parse(listenaddr) + if err == nil { + switch strings.ToLower(u.Scheme) { + case "unix": + if _, err := os.Stat(u.Path); err == nil { + a.log.Debugln("Admin socket", u.Path, "already exists, trying to clean up") + if _, err := net.DialTimeout("unix", u.Path, time.Second*2); err == nil || err.(net.Error).Timeout() { + a.log.Errorln("Admin socket", u.Path, "already exists and is in use by another process") + os.Exit(1) + } else { + if err := os.Remove(u.Path); err == nil { + a.log.Debugln(u.Path, "was cleaned up") + } else { + a.log.Errorln(u.Path, "already exists and was not cleaned up:", err) + os.Exit(1) + } + } + } + a.listener, err = net.Listen("unix", u.Path) + if err == nil { + switch u.Path[:1] { + case "@": // maybe abstract namespace + default: + if err := os.Chmod(u.Path, 0660); err != nil { + a.log.Warnln("WARNING:", u.Path, "may have unsafe permissions!") + } + } + } + case "tcp": + a.listener, err = net.Listen("tcp", u.Host) + default: + a.listener, err = net.Listen("tcp", listenaddr) + } + } else { + a.listener, err = net.Listen("tcp", listenaddr) + } + if err != nil { + a.log.Errorf("Admin socket failed to listen: %v", err) + os.Exit(1) + } + a.log.Infof("%s admin socket listening on %s", + strings.ToUpper(a.listener.Addr().Network()), + a.listener.Addr().String()) + + _ = a.AddHandler("list", "List available commands", []string{}, func(_ json.RawMessage) (interface{}, error) { + res := &ListResponse{} + for name, handler := range a.handlers { + res.List = append(res.List, ListEntry{ + Command: name, + Description: handler.desc, + Fields: handler.args, + }) + } + sort.SliceStable(res.List, func(i, j int) bool { + return strings.Compare(res.List[i].Command, res.List[j].Command) < 0 + }) + return res, nil + }) + a.done = make(chan struct{}) + go a.listen() + return a, a.core.SetAdmin(a) +} + +func (a *AdminSocket) SetupAdminHandlers() { + _ = a.AddHandler( + "getSelf", "Show details about this node", []string{}, + func(in json.RawMessage) (interface{}, error) { + req := &GetSelfRequest{} + res := &GetSelfResponse{} + if err := json.Unmarshal(in, &req); err != nil { + return nil, err + } + if err := a.getSelfHandler(req, res); err != nil { + return nil, err + } + return res, nil + }, + ) + _ = a.AddHandler( + "getPeers", "Show directly connected peers", []string{}, + func(in json.RawMessage) (interface{}, error) { + req := &GetPeersRequest{} + res := &GetPeersResponse{} + if err := json.Unmarshal(in, &req); err != nil { + return nil, err + } + if err := a.getPeersHandler(req, res); err != nil { + return nil, err + } + return res, nil + }, + ) + _ = a.AddHandler( + "getTree", "Show known Tree entries", []string{}, + func(in json.RawMessage) (interface{}, error) { + req := &GetTreeRequest{} + res := &GetTreeResponse{} + if err := json.Unmarshal(in, &req); err != nil { + return nil, err + } + if err := a.getTreeHandler(req, res); err != nil { + return nil, err + } + return res, nil + }, + ) + _ = a.AddHandler( + "getPaths", "Show established paths through this node", []string{}, + func(in json.RawMessage) (interface{}, error) { + req := &GetPathsRequest{} + res := &GetPathsResponse{} + if err := json.Unmarshal(in, &req); err != nil { + return nil, err + } + if err := a.getPathsHandler(req, res); err != nil { + return nil, err + } + return res, nil + }, + ) + _ = a.AddHandler( + "getSessions", "Show established traffic sessions with remote nodes", []string{}, + func(in json.RawMessage) (interface{}, error) { + req := &GetSessionsRequest{} + res := &GetSessionsResponse{} + if err := json.Unmarshal(in, &req); err != nil { + return nil, err + } + if err := a.getSessionsHandler(req, res); err != nil { + return nil, err + } + return res, nil + }, + ) + _ = a.AddHandler( + "addPeer", "Add a peer to the peer list", []string{"uri", "interface"}, + func(in json.RawMessage) (interface{}, error) { + req := &AddPeerRequest{} + res := &AddPeerResponse{} + if err := json.Unmarshal(in, &req); err != nil { + return nil, err + } + if err := a.addPeerHandler(req, res); err != nil { + return nil, err + } + return res, nil + }, + ) + _ = a.AddHandler( + "removePeer", "Remove a peer from the peer list", []string{"uri", "interface"}, + func(in json.RawMessage) (interface{}, error) { + req := &RemovePeerRequest{} + res := &RemovePeerResponse{} + if err := json.Unmarshal(in, &req); err != nil { + return nil, err + } + if err := a.removePeerHandler(req, res); err != nil { + return nil, err + } + return res, nil + }, + ) } // IsStarted returns true if the module has been started. @@ -171,6 +263,9 @@ func (a *AdminSocket) IsStarted() bool { // Stop will stop the admin API and close the socket. func (a *AdminSocket) Stop() error { + if a == nil { + return nil + } if a.listener != nil { select { case <-a.done: @@ -184,50 +279,6 @@ func (a *AdminSocket) Stop() error { // listen is run by start and manages API connections. func (a *AdminSocket) listen() { - u, err := url.Parse(a.listenaddr) - if err == nil { - switch strings.ToLower(u.Scheme) { - case "unix": - if _, err := os.Stat(a.listenaddr[7:]); err == nil { - a.log.Debugln("Admin socket", a.listenaddr[7:], "already exists, trying to clean up") - if _, err := net.DialTimeout("unix", a.listenaddr[7:], time.Second*2); err == nil || err.(net.Error).Timeout() { - a.log.Errorln("Admin socket", a.listenaddr[7:], "already exists and is in use by another process") - os.Exit(1) - } else { - if err := os.Remove(a.listenaddr[7:]); err == nil { - a.log.Debugln(a.listenaddr[7:], "was cleaned up") - } else { - a.log.Errorln(a.listenaddr[7:], "already exists and was not cleaned up:", err) - os.Exit(1) - } - } - } - a.listener, err = net.Listen("unix", a.listenaddr[7:]) - if err == nil { - switch a.listenaddr[7:8] { - case "@": // maybe abstract namespace - default: - if err := os.Chmod(a.listenaddr[7:], 0660); err != nil { - a.log.Warnln("WARNING:", a.listenaddr[:7], "may have unsafe permissions!") - } - } - } - case "tcp": - a.listener, err = net.Listen("tcp", u.Host) - default: - // err = errors.New(fmt.Sprint("protocol not supported: ", u.Scheme)) - a.listener, err = net.Listen("tcp", a.listenaddr) - } - } else { - a.listener, err = net.Listen("tcp", a.listenaddr) - } - if err != nil { - a.log.Errorf("Admin socket failed to listen: %v", err) - os.Exit(1) - } - a.log.Infof("%s admin socket listening on %s", - strings.ToUpper(a.listener.Addr().Network()), - a.listener.Addr().String()) defer a.listener.Close() for { conn, err := a.listener.Accept() @@ -255,53 +306,65 @@ func (a *AdminSocket) handleRequest(conn net.Conn) { defer conn.Close() - defer func() { - r := recover() - if r != nil { - a.log.Debugln("Admin socket error:", r) - if err := encoder.Encode(&ErrorResponse{ - Error: "Check your syntax and input types", - }); err != nil { - a.log.Debugln("Admin socket JSON encode error:", err) - } - conn.Close() - } - }() - for { var err error var buf json.RawMessage - _ = decoder.Decode(&buf) + var req AdminSocketRequest var resp AdminSocketResponse - resp.Status = "success" - if err = json.Unmarshal(buf, &resp.Request); err == nil { - if resp.Request.Name == "" { - resp.Status = "error" - resp.Response = &ErrorResponse{ - Error: "No request specified", - } - } else if h, ok := a.handlers[strings.ToLower(resp.Request.Name)]; ok { - resp.Response, err = h.handler(buf) - if err != nil { - resp.Status = "error" - resp.Response = &ErrorResponse{ - Error: err.Error(), - } - } - } else { - resp.Status = "error" - resp.Response = &ErrorResponse{ - Error: fmt.Sprintf("Unknown action '%s', try 'list' for help", resp.Request.Name), - } + req.Arguments = []byte("{}") + if err := func() error { + if err = decoder.Decode(&buf); err != nil { + return fmt.Errorf("Failed to find request") } + if err = json.Unmarshal(buf, &req); err != nil { + return fmt.Errorf("Failed to unmarshal request") + } + resp.Request = req + if req.Name == "" { + return fmt.Errorf("No request specified") + } + reqname := strings.ToLower(req.Name) + handler, ok := a.handlers[reqname] + if !ok { + return fmt.Errorf("Unknown action '%s', try 'list' for help", reqname) + } + res, err := handler.handler(req.Arguments) + if err != nil { + return err + } + if resp.Response, err = json.Marshal(res); err != nil { + return fmt.Errorf("Failed to marshal response: %w", err) + } + resp.Status = "success" + return nil + }(); err != nil { + resp.Status = "error" + resp.Error = err.Error() } if err = encoder.Encode(resp); err != nil { a.log.Debugln("Encode error:", err) } - if !resp.Request.KeepAlive { + if !req.KeepAlive { break } else { continue } } } + +type DataUnit uint64 + +func (d DataUnit) String() string { + switch { + case d >= 1024*1024*1024*1024: + return fmt.Sprintf("%2.1fTB", float64(d)/1024/1024/1024/1024) + case d >= 1024*1024*1024: + return fmt.Sprintf("%2.1fGB", float64(d)/1024/1024/1024) + case d >= 1024*1024: + return fmt.Sprintf("%2.1fMB", float64(d)/1024/1024) + case d >= 100: + return fmt.Sprintf("%2.1fKB", float64(d)/1024) + default: + return fmt.Sprintf("%dB", d) + } +} diff --git a/src/admin/getdht.go b/src/admin/getdht.go deleted file mode 100644 index 5dc95547..00000000 --- a/src/admin/getdht.go +++ /dev/null @@ -1,34 +0,0 @@ -package admin - -import ( - "encoding/hex" - "net" - - "github.com/yggdrasil-network/yggdrasil-go/src/address" -) - -type GetDHTRequest struct{} - -type GetDHTResponse struct { - DHT map[string]DHTEntry `json:"dht"` -} - -type DHTEntry struct { - PublicKey string `json:"key"` - Port uint64 `json:"port"` - Rest uint64 `json:"rest"` -} - -func (a *AdminSocket) getDHTHandler(req *GetDHTRequest, res *GetDHTResponse) error { - res.DHT = map[string]DHTEntry{} - for _, d := range a.core.GetDHT() { - addr := address.AddrForKey(d.Key) - so := net.IP(addr[:]).String() - res.DHT[so] = DHTEntry{ - PublicKey: hex.EncodeToString(d.Key[:]), - Port: d.Port, - Rest: d.Rest, - } - } - return nil -} diff --git a/src/admin/getpaths.go b/src/admin/getpaths.go index c8e97d01..250f4c6a 100644 --- a/src/admin/getpaths.go +++ b/src/admin/getpaths.go @@ -3,6 +3,8 @@ package admin import ( "encoding/hex" "net" + "slices" + "strings" "github.com/yggdrasil-network/yggdrasil-go/src/address" ) @@ -11,23 +13,30 @@ type GetPathsRequest struct { } type GetPathsResponse struct { - Paths map[string]PathEntry `json:"paths"` + Paths []PathEntry `json:"paths"` } type PathEntry struct { + IPAddress string `json:"address"` PublicKey string `json:"key"` Path []uint64 `json:"path"` + Sequence uint64 `json:"sequence"` } -func (a *AdminSocket) getPathsHandler(req *GetPathsRequest, res *GetPathsResponse) error { - res.Paths = map[string]PathEntry{} - for _, p := range a.core.GetPaths() { +func (a *AdminSocket) getPathsHandler(_ *GetPathsRequest, res *GetPathsResponse) error { + paths := a.core.GetPaths() + res.Paths = make([]PathEntry, 0, len(paths)) + for _, p := range paths { addr := address.AddrForKey(p.Key) - so := net.IP(addr[:]).String() - res.Paths[so] = PathEntry{ + res.Paths = append(res.Paths, PathEntry{ + IPAddress: net.IP(addr[:]).String(), PublicKey: hex.EncodeToString(p.Key), Path: p.Path, - } + Sequence: p.Sequence, + }) } + slices.SortStableFunc(res.Paths, func(a, b PathEntry) int { + return strings.Compare(a.PublicKey, b.PublicKey) + }) return nil } diff --git a/src/admin/getpeers.go b/src/admin/getpeers.go index ecb28726..0384b792 100644 --- a/src/admin/getpeers.go +++ b/src/admin/getpeers.go @@ -3,6 +3,9 @@ package admin import ( "encoding/hex" "net" + "slices" + "strings" + "time" "github.com/yggdrasil-network/yggdrasil-go/src/address" ) @@ -11,33 +14,78 @@ type GetPeersRequest struct { } type GetPeersResponse struct { - Peers map[string]PeerEntry `json:"peers"` + Peers []PeerEntry `json:"peers"` } type PeerEntry struct { - PublicKey string `json:"key"` - Port uint64 `json:"port"` - Coords []uint64 `json:"coords"` - Remote string `json:"remote"` - RXBytes uint64 `json:"bytes_recvd"` - TXBytes uint64 `json:"bytes_sent"` - Uptime float64 `json:"uptime"` + URI string `json:"remote,omitempty"` + Up bool `json:"up"` + Inbound bool `json:"inbound"` + IPAddress string `json:"address,omitempty"` + PublicKey string `json:"key"` + Port uint64 `json:"port"` + Priority uint64 `json:"priority"` + Cost uint64 `json:"cost"` + RXBytes DataUnit `json:"bytes_recvd,omitempty"` + TXBytes DataUnit `json:"bytes_sent,omitempty"` + RXRate DataUnit `json:"rate_recvd,omitempty"` + TXRate DataUnit `json:"rate_sent,omitempty"` + Uptime float64 `json:"uptime,omitempty"` + Latency time.Duration `json:"latency,omitempty"` + LastErrorTime time.Duration `json:"last_error_time,omitempty"` + LastError string `json:"last_error,omitempty"` } -func (a *AdminSocket) getPeersHandler(req *GetPeersRequest, res *GetPeersResponse) error { - res.Peers = map[string]PeerEntry{} - for _, p := range a.core.GetPeers() { - addr := address.AddrForKey(p.Key) - so := net.IP(addr[:]).String() - res.Peers[so] = PeerEntry{ - PublicKey: hex.EncodeToString(p.Key), - Port: p.Port, - Coords: p.Coords, - Remote: p.Remote, - RXBytes: p.RXBytes, - TXBytes: p.TXBytes, - Uptime: p.Uptime.Seconds(), +func (a *AdminSocket) getPeersHandler(_ *GetPeersRequest, res *GetPeersResponse) error { + peers := a.core.GetPeers() + res.Peers = make([]PeerEntry, 0, len(peers)) + for _, p := range peers { + peer := PeerEntry{ + Port: p.Port, + Up: p.Up, + Inbound: p.Inbound, + Priority: uint64(p.Priority), // can't be uint8 thanks to gobind + Cost: p.Cost, + URI: p.URI, + RXBytes: DataUnit(p.RXBytes), + TXBytes: DataUnit(p.TXBytes), + RXRate: DataUnit(p.RXRate), + TXRate: DataUnit(p.TXRate), + Uptime: p.Uptime.Seconds(), } + if p.Latency > 0 { + peer.Latency = p.Latency + } + if addr := address.AddrForKey(p.Key); addr != nil { + peer.PublicKey = hex.EncodeToString(p.Key) + peer.IPAddress = net.IP(addr[:]).String() + } + if p.LastError != nil { + peer.LastError = p.LastError.Error() + peer.LastErrorTime = time.Since(p.LastErrorTime) + } + res.Peers = append(res.Peers, peer) } + slices.SortStableFunc(res.Peers, func(a, b PeerEntry) int { + if !a.Inbound && b.Inbound { + return -1 + } + if a.Inbound && !b.Inbound { + return 1 + } + if d := strings.Compare(a.PublicKey, b.PublicKey); d != 0 { + return d + } + if d := a.Priority - b.Priority; d != 0 { + return int(d) + } + if d := a.Cost - b.Cost; d != 0 { + return int(d) + } + if d := a.Uptime - b.Uptime; d != 0 { + return int(d) + } + return 0 + }) return nil } diff --git a/src/admin/getself.go b/src/admin/getself.go index 7effcc46..b1a01567 100644 --- a/src/admin/getself.go +++ b/src/admin/getself.go @@ -9,28 +9,22 @@ import ( type GetSelfRequest struct{} type GetSelfResponse struct { - Self map[string]SelfEntry `json:"self"` + BuildName string `json:"build_name"` + BuildVersion string `json:"build_version"` + PublicKey string `json:"key"` + IPAddress string `json:"address"` + RoutingEntries uint64 `json:"routing_entries"` + Subnet string `json:"subnet"` } -type SelfEntry struct { - BuildName string `json:"build_name"` - BuildVersion string `json:"build_version"` - PublicKey string `json:"key"` - Coords []uint64 `json:"coords"` - Subnet string `json:"subnet"` -} - -func (a *AdminSocket) getSelfHandler(req *GetSelfRequest, res *GetSelfResponse) error { - res.Self = make(map[string]SelfEntry) +func (a *AdminSocket) getSelfHandler(_ *GetSelfRequest, res *GetSelfResponse) error { self := a.core.GetSelf() - addr := a.core.Address().String() snet := a.core.Subnet() - res.Self[addr] = SelfEntry{ - BuildName: version.BuildName(), - BuildVersion: version.BuildVersion(), - PublicKey: hex.EncodeToString(self.Key[:]), - Subnet: snet.String(), - Coords: self.Coords, - } + res.BuildName = version.BuildName() + res.BuildVersion = version.BuildVersion() + res.PublicKey = hex.EncodeToString(self.Key[:]) + res.IPAddress = a.core.Address().String() + res.Subnet = snet.String() + res.RoutingEntries = self.RoutingEntries return nil } diff --git a/src/admin/getsessions.go b/src/admin/getsessions.go index 3a0c19b6..2d76a35b 100644 --- a/src/admin/getsessions.go +++ b/src/admin/getsessions.go @@ -3,6 +3,8 @@ package admin import ( "encoding/hex" "net" + "slices" + "strings" "github.com/yggdrasil-network/yggdrasil-go/src/address" ) @@ -10,21 +12,32 @@ import ( type GetSessionsRequest struct{} type GetSessionsResponse struct { - Sessions map[string]SessionEntry `json:"sessions"` + Sessions []SessionEntry `json:"sessions"` } type SessionEntry struct { - PublicKey string `json:"key"` + IPAddress string `json:"address"` + PublicKey string `json:"key"` + RXBytes DataUnit `json:"bytes_recvd"` + TXBytes DataUnit `json:"bytes_sent"` + Uptime float64 `json:"uptime"` } -func (a *AdminSocket) getSessionsHandler(req *GetSessionsRequest, res *GetSessionsResponse) error { - res.Sessions = map[string]SessionEntry{} - for _, s := range a.core.GetSessions() { +func (a *AdminSocket) getSessionsHandler(_ *GetSessionsRequest, res *GetSessionsResponse) error { + sessions := a.core.GetSessions() + res.Sessions = make([]SessionEntry, 0, len(sessions)) + for _, s := range sessions { addr := address.AddrForKey(s.Key) - so := net.IP(addr[:]).String() - res.Sessions[so] = SessionEntry{ + res.Sessions = append(res.Sessions, SessionEntry{ + IPAddress: net.IP(addr[:]).String(), PublicKey: hex.EncodeToString(s.Key[:]), - } + RXBytes: DataUnit(s.RXBytes), + TXBytes: DataUnit(s.TXBytes), + Uptime: s.Uptime.Seconds(), + }) } + slices.SortStableFunc(res.Sessions, func(a, b SessionEntry) int { + return strings.Compare(a.PublicKey, b.PublicKey) + }) return nil } diff --git a/src/admin/gettree.go b/src/admin/gettree.go new file mode 100644 index 00000000..993827d9 --- /dev/null +++ b/src/admin/gettree.go @@ -0,0 +1,41 @@ +package admin + +import ( + "encoding/hex" + "net" + "slices" + "strings" + + "github.com/yggdrasil-network/yggdrasil-go/src/address" +) + +type GetTreeRequest struct{} + +type GetTreeResponse struct { + Tree []TreeEntry `json:"tree"` +} + +type TreeEntry struct { + IPAddress string `json:"address"` + PublicKey string `json:"key"` + Parent string `json:"parent"` + Sequence uint64 `json:"sequence"` +} + +func (a *AdminSocket) getTreeHandler(_ *GetTreeRequest, res *GetTreeResponse) error { + tree := a.core.GetTree() + res.Tree = make([]TreeEntry, 0, len(tree)) + for _, d := range tree { + addr := address.AddrForKey(d.Key) + res.Tree = append(res.Tree, TreeEntry{ + IPAddress: net.IP(addr[:]).String(), + PublicKey: hex.EncodeToString(d.Key[:]), + Parent: hex.EncodeToString(d.Parent[:]), + Sequence: d.Sequence, + }) + } + slices.SortStableFunc(res.Tree, func(a, b TreeEntry) int { + return strings.Compare(a.PublicKey, b.PublicKey) + }) + return nil +} diff --git a/src/admin/options.go b/src/admin/options.go new file mode 100644 index 00000000..03cf74ab --- /dev/null +++ b/src/admin/options.go @@ -0,0 +1,79 @@ +package admin + +import ( + "crypto/ed25519" + "encoding/hex" + "encoding/json" + "net" + "sync" + "time" + + "github.com/Arceliar/ironwood/network" + + "github.com/yggdrasil-network/yggdrasil-go/src/address" +) + +func (c *AdminSocket) _applyOption(opt SetupOption) { + switch v := opt.(type) { + case ListenAddress: + c.config.listenaddr = v + case LogLookups: + c.logLookups() + } +} + +type SetupOption interface { + isSetupOption() +} + +type ListenAddress string + +func (a ListenAddress) isSetupOption() {} + +type LogLookups struct{} + +func (l LogLookups) isSetupOption() {} + +func (a *AdminSocket) logLookups() { + type resi struct { + Address string `json:"addr"` + Key string `json:"key"` + Path []uint64 `json:"path"` + Time int64 `json:"time"` + } + type res struct { + Infos []resi `json:"infos"` + } + type info struct { + path []uint64 + time time.Time + } + type edk [ed25519.PublicKeySize]byte + infos := make(map[edk]info) + var m sync.Mutex + a.core.PacketConn.PacketConn.Debug.SetDebugLookupLogger(func(l network.DebugLookupInfo) { + var k edk + copy(k[:], l.Key[:]) + m.Lock() + infos[k] = info{path: l.Path, time: time.Now()} + m.Unlock() + }) + _ = a.AddHandler( + "lookups", "Dump a record of lookups received in the past hour", []string{}, + func(in json.RawMessage) (interface{}, error) { + m.Lock() + rs := make([]resi, 0, len(infos)) + for k, v := range infos { + if time.Since(v.time) > 24*time.Hour { + // TODO? automatic cleanup, so we don't need to call lookups periodically to prevent leaks + delete(infos, k) + } + a := address.AddrForKey(ed25519.PublicKey(k[:])) + addr := net.IP(a[:]).String() + rs = append(rs, resi{Address: addr, Key: hex.EncodeToString(k[:]), Path: v.path, Time: v.time.Unix()}) + } + m.Unlock() + return &res{Infos: rs}, nil + }, + ) +} diff --git a/src/admin/removepeer.go b/src/admin/removepeer.go new file mode 100644 index 00000000..6f368656 --- /dev/null +++ b/src/admin/removepeer.go @@ -0,0 +1,21 @@ +package admin + +import ( + "fmt" + "net/url" +) + +type RemovePeerRequest struct { + Uri string `json:"uri"` + Sintf string `json:"interface,omitempty"` +} + +type RemovePeerResponse struct{} + +func (a *AdminSocket) removePeerHandler(req *RemovePeerRequest, _ *RemovePeerResponse) error { + u, err := url.Parse(req.Uri) + if err != nil { + return fmt.Errorf("unable to parse peering URI: %w", err) + } + return a.core.RemovePeer(u, req.Sintf) +} diff --git a/src/config/config.go b/src/config/config.go index 041147b8..5dd7b3d4 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -17,45 +17,244 @@ configuration option that is not provided. package config import ( + "bytes" "crypto/ed25519" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" "encoding/hex" - "sync" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "math/big" + "os" + "time" + + "github.com/hjson/hjson-go/v4" + "golang.org/x/text/encoding/unicode" ) // NodeConfig is the main configuration structure, containing configuration // options that are necessary for an Yggdrasil node to run. You will need to // supply one of these structs to the Yggdrasil core when starting a node. type NodeConfig struct { - sync.RWMutex `json:"-"` - Peers []string `comment:"List of connection strings for outbound peer connections in URI format,\ne.g. tls://a.b.c.d:e or socks://a.b.c.d:e/f.g.h.i:j. These connections\nwill obey the operating system routing table, therefore you should\nuse this section when you may connect via different interfaces."` - InterfacePeers map[string][]string `comment:"List of connection strings for outbound peer connections in URI format,\narranged by source interface, e.g. { \"eth0\": [ tls://a.b.c.d:e ] }.\nNote that SOCKS peerings will NOT be affected by this option and should\ngo in the \"Peers\" section instead."` - Listen []string `comment:"Listen addresses for incoming connections. You will need to add\nlisteners in order to accept incoming peerings from non-local nodes.\nMulticast peer discovery will work regardless of any listeners set\nhere. Each listener should be specified in URI format as above, e.g.\ntls://0.0.0.0:0 or tls://[::]:0 to listen on all interfaces."` - AdminListen string `comment:"Listen address for admin connections. Default is to listen for local\nconnections either on TCP/9001 or a UNIX socket depending on your\nplatform. Use this value for yggdrasilctl -endpoint=X. To disable\nthe admin socket, use the value \"none\" instead."` - MulticastInterfaces []MulticastInterfaceConfig `comment:"Configuration for which interfaces multicast peer discovery should be\nenabled on. Each entry in the list should be a json object which may\ncontain Regex, Beacon, Listen, and Port. Regex is a regular expression\nwhich is matched against an interface name, and interfaces use the\nfirst configuration that they match gainst. Beacon configures whether\nor not the node should send link-local multicast beacons to advertise\ntheir presence, while listening for incoming connections on Port.\nListen controls whether or not the node listens for multicast beacons\nand opens outgoing connections."` - AllowedPublicKeys []string `comment:"List of peer public keys to allow incoming peering connections\nfrom. If left empty/undefined then all connections will be allowed\nby default. This does not affect outgoing peerings, nor does it\naffect link-local peers discovered via multicast."` - PublicKey string `comment:"Your public key. Your peers may ask you for this to put\ninto their AllowedPublicKeys configuration."` - PrivateKey string `comment:"Your private key. DO NOT share this with anyone!"` + PrivateKey KeyBytes `json:",omitempty" comment:"Your private key. DO NOT share this with anyone!"` + PrivateKeyPath string `json:",omitempty" comment:"The path to your private key file in PEM format."` + Certificate *tls.Certificate `json:"-"` + Peers []string `comment:"List of outbound peer connection strings (e.g. tls://a.b.c.d:e or\nsocks://a.b.c.d:e/f.g.h.i:j). Connection strings can contain options,\nsee https://yggdrasil-network.github.io/configurationref.html#peers.\nYggdrasil has no concept of bootstrap nodes - all network traffic\nwill transit peer connections. Therefore make sure to only peer with\nnearby nodes that have good connectivity and low latency. Avoid adding\npeers to this list from distant countries as this will worsen your\nnode's connectivity and performance considerably."` + InterfacePeers map[string][]string `comment:"List of connection strings for outbound peer connections in URI format,\narranged by source interface, e.g. { \"eth0\": [ \"tls://a.b.c.d:e\" ] }.\nYou should only use this option if your machine is multi-homed and you\nwant to establish outbound peer connections on different interfaces.\nOtherwise you should use \"Peers\"."` + Listen []string `comment:"Listen addresses for incoming connections. You will need to add\nlisteners in order to accept incoming peerings from non-local nodes.\nThis is not required if you wish to establish outbound peerings only.\nMulticast peer discovery will work regardless of any listeners set\nhere. Each listener should be specified in URI format as above, e.g.\ntls://0.0.0.0:0 or tls://[::]:0 to listen on all interfaces."` + AdminListen string `json:",omitempty" comment:"Listen address for admin connections. Default is to listen for local\nconnections either on TCP/9001 or a UNIX socket depending on your\nplatform. Use this value for yggdrasilctl -endpoint=X. To disable\nthe admin socket, use the value \"none\" instead."` + MulticastInterfaces []MulticastInterfaceConfig `comment:"Configuration for which interfaces multicast peer discovery should be\nenabled on. Regex is a regular expression which is matched against an\ninterface name, and interfaces use the first configuration that they\nmatch against. Beacon controls whether or not your node advertises its\npresence to others, whereas Listen controls whether or not your node\nlistens out for and tries to connect to other advertising nodes. See\nhttps://yggdrasil-network.github.io/configurationref.html#multicastinterfaces\nfor more supported options."` + AllowedPublicKeys []string `comment:"List of peer public keys to allow incoming peering connections\nfrom. If left empty/undefined then all connections will be allowed\nby default. This does not affect outgoing peerings, nor does it\naffect link-local peers discovered via multicast.\nWARNING: THIS IS NOT A FIREWALL and DOES NOT limit who can reach\nopen ports or services running on your machine!"` IfName string `comment:"Local network interface name for TUN adapter, or \"auto\" to select\nan interface automatically, or \"none\" to run without TUN."` IfMTU uint64 `comment:"Maximum Transmission Unit (MTU) size for your local TUN interface.\nDefault is the largest supported size for your platform. The lowest\npossible value is 1280."` + LogLookups bool `json:",omitempty"` NodeInfoPrivacy bool `comment:"By default, nodeinfo contains some defaults including the platform,\narchitecture and Yggdrasil version. These can help when surveying\nthe network and diagnosing network routing problems. Enabling\nnodeinfo privacy prevents this, so that only items specified in\n\"NodeInfo\" are sent back if specified."` - NodeInfo map[string]interface{} `comment:"Optional node info. This must be a { \"key\": \"value\", ... } map\nor set as null. This is entirely optional but, if set, is visible\nto the whole network on request."` + NodeInfo map[string]interface{} `comment:"Optional nodeinfo. This must be a { \"key\": \"value\", ... } map\nor set as null. This is entirely optional but, if set, is visible\nto the whole network on request."` } type MulticastInterfaceConfig struct { - Regex string - Beacon bool - Listen bool - Port uint16 + Regex string + Beacon bool + Listen bool + Port uint16 `json:",omitempty"` + Priority uint64 `json:",omitempty"` // really uint8, but gobind won't export it + Password string } -// NewSigningKeys replaces the signing keypair in the NodeConfig with a new -// signing keypair. The signing keys are used by the switch to derive the -// structure of the spanning tree. -func (cfg *NodeConfig) NewKeys() { - spub, spriv, err := ed25519.GenerateKey(nil) +// Generates default configuration and returns a pointer to the resulting +// NodeConfig. This is used when outputting the -genconf parameter and also when +// using -autoconf. +func GenerateConfig() *NodeConfig { + // Get the defaults for the platform. + defaults := GetDefaults() + // Create a node configuration and populate it. + cfg := new(NodeConfig) + cfg.NewPrivateKey() + cfg.Listen = []string{} + cfg.AdminListen = defaults.DefaultAdminListen + cfg.Peers = []string{} + cfg.InterfacePeers = map[string][]string{} + cfg.AllowedPublicKeys = []string{} + cfg.MulticastInterfaces = defaults.DefaultMulticastInterfaces + cfg.IfName = defaults.DefaultIfName + cfg.IfMTU = defaults.DefaultIfMTU + cfg.NodeInfoPrivacy = false + if err := cfg.postprocessConfig(); err != nil { + panic(err) + } + return cfg +} + +func (cfg *NodeConfig) ReadFrom(r io.Reader) (int64, error) { + conf, err := io.ReadAll(r) + if err != nil { + return 0, err + } + n := int64(len(conf)) + // If there's a byte order mark - which Windows 10 is now incredibly fond of + // throwing everywhere when it's converting things into UTF-16 for the hell + // of it - remove it and decode back down into UTF-8. This is necessary + // because hjson doesn't know what to do with UTF-16 and will panic + if bytes.Equal(conf[0:2], []byte{0xFF, 0xFE}) || + bytes.Equal(conf[0:2], []byte{0xFE, 0xFF}) { + utf := unicode.UTF16(unicode.BigEndian, unicode.UseBOM) + decoder := utf.NewDecoder() + conf, err = decoder.Bytes(conf) + if err != nil { + return n, err + } + } + // Generate a new configuration - this gives us a set of sane defaults - + // then parse the configuration we loaded above on top of it. The effect + // of this is that any configuration item that is missing from the provided + // configuration will use a sane default. + *cfg = *GenerateConfig() + if err := cfg.UnmarshalHJSON(conf); err != nil { + return n, err + } + return n, nil +} + +func (cfg *NodeConfig) UnmarshalHJSON(b []byte) error { + if err := hjson.Unmarshal(b, cfg); err != nil { + return err + } + return cfg.postprocessConfig() +} + +func (cfg *NodeConfig) postprocessConfig() error { + if cfg.PrivateKeyPath != "" { + cfg.PrivateKey = nil + f, err := os.ReadFile(cfg.PrivateKeyPath) + if err != nil { + return err + } + if err := cfg.UnmarshalPEMPrivateKey(f); err != nil { + return err + } + } + switch { + case cfg.Certificate == nil: + // No self-signed certificate has been generated yet. + fallthrough + case !bytes.Equal(cfg.Certificate.PrivateKey.(ed25519.PrivateKey), cfg.PrivateKey): + // A self-signed certificate was generated but the private + // key has changed since then, possibly because a new config + // was parsed. + if err := cfg.GenerateSelfSignedCertificate(); err != nil { + return err + } + } + return nil +} + +// RFC5280 section 4.1.2.5 +var notAfterNeverExpires = time.Date(9999, time.December, 31, 23, 59, 59, 0, time.UTC) + +func (cfg *NodeConfig) GenerateSelfSignedCertificate() error { + key, err := cfg.MarshalPEMPrivateKey() + if err != nil { + return err + } + cert, err := cfg.MarshalPEMCertificate() + if err != nil { + return err + } + tlsCert, err := tls.X509KeyPair(cert, key) + if err != nil { + return err + } + cfg.Certificate = &tlsCert + return nil +} + +func (cfg *NodeConfig) MarshalPEMCertificate() ([]byte, error) { + privateKey := ed25519.PrivateKey(cfg.PrivateKey) + publicKey := privateKey.Public().(ed25519.PublicKey) + + cert := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: hex.EncodeToString(publicKey), + }, + NotBefore: time.Now(), + NotAfter: notAfterNeverExpires, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + certbytes, err := x509.CreateCertificate(rand.Reader, cert, cert, publicKey, privateKey) + if err != nil { + return nil, err + } + + block := &pem.Block{ + Type: "CERTIFICATE", + Bytes: certbytes, + } + return pem.EncodeToMemory(block), nil +} + +func (cfg *NodeConfig) NewPrivateKey() { + _, spriv, err := ed25519.GenerateKey(nil) if err != nil { panic(err) } - cfg.PublicKey = hex.EncodeToString(spub[:]) - cfg.PrivateKey = hex.EncodeToString(spriv[:]) + cfg.PrivateKey = KeyBytes(spriv) +} + +func (cfg *NodeConfig) MarshalPEMPrivateKey() ([]byte, error) { + b, err := x509.MarshalPKCS8PrivateKey(ed25519.PrivateKey(cfg.PrivateKey)) + if err != nil { + return nil, fmt.Errorf("failed to marshal PKCS8 key: %w", err) + } + block := &pem.Block{ + Type: "PRIVATE KEY", + Bytes: b, + } + return pem.EncodeToMemory(block), nil +} + +func (cfg *NodeConfig) UnmarshalPEMPrivateKey(b []byte) error { + p, _ := pem.Decode(b) + if p == nil { + return fmt.Errorf("failed to parse PEM file") + } + if p.Type != "PRIVATE KEY" { + return fmt.Errorf("unexpected PEM type %q", p.Type) + } + k, err := x509.ParsePKCS8PrivateKey(p.Bytes) + if err != nil { + return fmt.Errorf("failed to unmarshal PKCS8 key: %w", err) + } + key, ok := k.(ed25519.PrivateKey) + if !ok { + return fmt.Errorf("private key must be ed25519 key") + } + if len(key) != ed25519.PrivateKeySize { + return fmt.Errorf("unexpected ed25519 private key length") + } + cfg.PrivateKey = KeyBytes(key) + return nil +} + +type KeyBytes []byte + +func (k KeyBytes) MarshalJSON() ([]byte, error) { + return json.Marshal(hex.EncodeToString(k)) +} + +func (k *KeyBytes) UnmarshalJSON(b []byte) error { + var s string + var err error + if err = json.Unmarshal(b, &s); err != nil { + return err + } + *k, err = hex.DecodeString(s) + return err } diff --git a/src/config/config_test.go b/src/config/config_test.go index 8b6e14e1..6b74b50f 100644 --- a/src/config/config_test.go +++ b/src/config/config_test.go @@ -1,54 +1,54 @@ package config import ( - "bytes" - "encoding/hex" "testing" ) func TestConfig_Keys(t *testing.T) { - var nodeConfig NodeConfig - nodeConfig.NewKeys() + /* + var nodeConfig NodeConfig + nodeConfig.NewKeys() - publicKey1, err := hex.DecodeString(nodeConfig.PublicKey) + publicKey1, err := hex.DecodeString(nodeConfig.PublicKey) - if err != nil { - t.Fatal("can not decode generated public key") - } + if err != nil { + t.Fatal("can not decode generated public key") + } - if len(publicKey1) == 0 { - t.Fatal("empty public key generated") - } + if len(publicKey1) == 0 { + t.Fatal("empty public key generated") + } - privateKey1, err := hex.DecodeString(nodeConfig.PrivateKey) + privateKey1, err := hex.DecodeString(nodeConfig.PrivateKey) - if err != nil { - t.Fatal("can not decode generated private key") - } + if err != nil { + t.Fatal("can not decode generated private key") + } - if len(privateKey1) == 0 { - t.Fatal("empty private key generated") - } + if len(privateKey1) == 0 { + t.Fatal("empty private key generated") + } - nodeConfig.NewKeys() + nodeConfig.NewKeys() - publicKey2, err := hex.DecodeString(nodeConfig.PublicKey) + publicKey2, err := hex.DecodeString(nodeConfig.PublicKey) - if err != nil { - t.Fatal("can not decode generated public key") - } + if err != nil { + t.Fatal("can not decode generated public key") + } - if bytes.Equal(publicKey2, publicKey1) { - t.Fatal("same public key generated") - } + if bytes.Equal(publicKey2, publicKey1) { + t.Fatal("same public key generated") + } - privateKey2, err := hex.DecodeString(nodeConfig.PrivateKey) + privateKey2, err := hex.DecodeString(nodeConfig.PrivateKey) - if err != nil { - t.Fatal("can not decode generated private key") - } + if err != nil { + t.Fatal("can not decode generated private key") + } - if bytes.Equal(privateKey2, privateKey1) { - t.Fatal("same private key generated") - } + if bytes.Equal(privateKey2, privateKey1) { + t.Fatal("same private key generated") + } + */ } diff --git a/src/config/defaults.go b/src/config/defaults.go new file mode 100644 index 00000000..aaccfd0c --- /dev/null +++ b/src/config/defaults.go @@ -0,0 +1,34 @@ +package config + +var defaultConfig = "" // LDFLAGS='-X github.com/yggdrasil-network/yggdrasil-go/src/config.defaultConfig=/path/to/config +var defaultAdminListen = "" // LDFLAGS='-X github.com/yggdrasil-network/yggdrasil-go/src/config.defaultAdminListen=unix://path/to/sock' + +// Defines which parameters are expected by default for configuration on a +// specific platform. These values are populated in the relevant defaults_*.go +// for the platform being targeted. They must be set. +type platformDefaultParameters struct { + // Admin socket + DefaultAdminListen string + + // Configuration (used for yggdrasilctl) + DefaultConfigFile string + + // Multicast interfaces + DefaultMulticastInterfaces []MulticastInterfaceConfig + + // TUN + MaximumIfMTU uint64 + DefaultIfMTU uint64 + DefaultIfName string +} + +func GetDefaults() platformDefaultParameters { + defaults := getDefaults() + if defaultConfig != "" { + defaults.DefaultConfigFile = defaultConfig + } + if defaultAdminListen != "" { + defaults.DefaultAdminListen = defaultAdminListen + } + return defaults +} diff --git a/src/defaults/defaults_darwin.go b/src/config/defaults_darwin.go similarity index 83% rename from src/defaults/defaults_darwin.go rename to src/config/defaults_darwin.go index 060ce814..5f44ef59 100644 --- a/src/defaults/defaults_darwin.go +++ b/src/config/defaults_darwin.go @@ -1,11 +1,11 @@ //go:build darwin // +build darwin -package defaults +package config // Sane defaults for the macOS/Darwin platform. The "default" options may be // may be replaced by the running configuration. -func GetDefaults() platformDefaultParameters { +func getDefaults() platformDefaultParameters { return platformDefaultParameters{ // Admin DefaultAdminListen: "unix:///var/run/yggdrasil.sock", @@ -17,9 +17,10 @@ func GetDefaults() platformDefaultParameters { DefaultMulticastInterfaces: []MulticastInterfaceConfig{ {Regex: "en.*", Beacon: true, Listen: true}, {Regex: "bridge.*", Beacon: true, Listen: true}, + {Regex: "awdl0", Beacon: false, Listen: false}, }, - // TUN/TAP + // TUN MaximumIfMTU: 65535, DefaultIfMTU: 65535, DefaultIfName: "auto", diff --git a/src/defaults/defaults_freebsd.go b/src/config/defaults_freebsd.go similarity index 88% rename from src/defaults/defaults_freebsd.go rename to src/config/defaults_freebsd.go index 84df48ad..97f7b4c3 100644 --- a/src/defaults/defaults_freebsd.go +++ b/src/config/defaults_freebsd.go @@ -1,11 +1,11 @@ //go:build freebsd // +build freebsd -package defaults +package config // Sane defaults for the BSD platforms. The "default" options may be // may be replaced by the running configuration. -func GetDefaults() platformDefaultParameters { +func getDefaults() platformDefaultParameters { return platformDefaultParameters{ // Admin DefaultAdminListen: "unix:///var/run/yggdrasil.sock", @@ -18,7 +18,7 @@ func GetDefaults() platformDefaultParameters { {Regex: ".*", Beacon: true, Listen: true}, }, - // TUN/TAP + // TUN MaximumIfMTU: 32767, DefaultIfMTU: 32767, DefaultIfName: "/dev/tun0", diff --git a/src/defaults/defaults_linux.go b/src/config/defaults_linux.go similarity index 87% rename from src/defaults/defaults_linux.go rename to src/config/defaults_linux.go index c7f5f119..6f7cbfc3 100644 --- a/src/defaults/defaults_linux.go +++ b/src/config/defaults_linux.go @@ -1,11 +1,11 @@ //go:build linux // +build linux -package defaults +package config // Sane defaults for the Linux platform. The "default" options may be // may be replaced by the running configuration. -func GetDefaults() platformDefaultParameters { +func getDefaults() platformDefaultParameters { return platformDefaultParameters{ // Admin DefaultAdminListen: "unix:///var/run/yggdrasil.sock", @@ -18,7 +18,7 @@ func GetDefaults() platformDefaultParameters { {Regex: ".*", Beacon: true, Listen: true}, }, - // TUN/TAP + // TUN MaximumIfMTU: 65535, DefaultIfMTU: 65535, DefaultIfName: "auto", diff --git a/src/defaults/defaults_openbsd.go b/src/config/defaults_openbsd.go similarity index 87% rename from src/defaults/defaults_openbsd.go rename to src/config/defaults_openbsd.go index 0ec877ca..81ddf7e8 100644 --- a/src/defaults/defaults_openbsd.go +++ b/src/config/defaults_openbsd.go @@ -1,11 +1,11 @@ //go:build openbsd // +build openbsd -package defaults +package config // Sane defaults for the BSD platforms. The "default" options may be // may be replaced by the running configuration. -func GetDefaults() platformDefaultParameters { +func getDefaults() platformDefaultParameters { return platformDefaultParameters{ // Admin DefaultAdminListen: "unix:///var/run/yggdrasil.sock", @@ -18,7 +18,7 @@ func GetDefaults() platformDefaultParameters { {Regex: ".*", Beacon: true, Listen: true}, }, - // TUN/TAP + // TUN MaximumIfMTU: 16384, DefaultIfMTU: 16384, DefaultIfName: "tun0", diff --git a/src/defaults/defaults_other.go b/src/config/defaults_other.go similarity index 89% rename from src/defaults/defaults_other.go rename to src/config/defaults_other.go index 37637425..8299364c 100644 --- a/src/defaults/defaults_other.go +++ b/src/config/defaults_other.go @@ -1,11 +1,11 @@ //go:build !linux && !darwin && !windows && !openbsd && !freebsd // +build !linux,!darwin,!windows,!openbsd,!freebsd -package defaults +package config // Sane defaults for the other platforms. The "default" options may be // may be replaced by the running configuration. -func GetDefaults() platformDefaultParameters { +func getDefaults() platformDefaultParameters { return platformDefaultParameters{ // Admin DefaultAdminListen: "tcp://localhost:9001", @@ -18,7 +18,7 @@ func GetDefaults() platformDefaultParameters { {Regex: ".*", Beacon: true, Listen: true}, }, - // TUN/TAP + // TUN MaximumIfMTU: 65535, DefaultIfMTU: 65535, DefaultIfName: "none", diff --git a/src/defaults/defaults_windows.go b/src/config/defaults_windows.go similarity index 88% rename from src/defaults/defaults_windows.go rename to src/config/defaults_windows.go index c1ea9689..5b30b4fd 100644 --- a/src/defaults/defaults_windows.go +++ b/src/config/defaults_windows.go @@ -1,11 +1,11 @@ //go:build windows // +build windows -package defaults +package config // Sane defaults for the Windows platform. The "default" options may be // may be replaced by the running configuration. -func GetDefaults() platformDefaultParameters { +func getDefaults() platformDefaultParameters { return platformDefaultParameters{ // Admin DefaultAdminListen: "tcp://localhost:9001", @@ -18,7 +18,7 @@ func GetDefaults() platformDefaultParameters { {Regex: ".*", Beacon: true, Listen: true}, }, - // TUN/TAP + // TUN MaximumIfMTU: 65535, DefaultIfMTU: 65535, DefaultIfName: "Yggdrasil", diff --git a/src/core/api.go b/src/core/api.go index fabd7439..cc1bde32 100644 --- a/src/core/api.go +++ b/src/core/api.go @@ -2,126 +2,149 @@ package core import ( "crypto/ed25519" + "encoding/json" + "net" + "net/url" "sync/atomic" "time" - //"encoding/hex" - "encoding/json" - //"errors" - //"fmt" - "net" - "net/url" + "github.com/Arceliar/phony" - //"sort" - //"time" - - "github.com/gologme/log" + "github.com/Arceliar/ironwood/network" "github.com/yggdrasil-network/yggdrasil-go/src/address" - //"github.com/yggdrasil-network/yggdrasil-go/src/crypto" - //"github.com/Arceliar/phony" ) -type Self struct { - Key ed25519.PublicKey - Root ed25519.PublicKey - Coords []uint64 +type SelfInfo struct { + Key ed25519.PublicKey + RoutingEntries uint64 } -type Peer struct { +type PeerInfo struct { + URI string + Up bool + Inbound bool + LastError error + LastErrorTime time.Time + Key ed25519.PublicKey + Root ed25519.PublicKey + Coords []uint64 + Port uint64 + Priority uint8 + Cost uint64 + RXBytes uint64 + TXBytes uint64 + RXRate uint64 + TXRate uint64 + Uptime time.Duration + Latency time.Duration +} + +type TreeEntryInfo struct { + Key ed25519.PublicKey + Parent ed25519.PublicKey + Sequence uint64 + //Port uint64 + //Rest uint64 +} + +type PathEntryInfo struct { + Key ed25519.PublicKey + Path []uint64 + Sequence uint64 +} + +type SessionInfo struct { Key ed25519.PublicKey - Root ed25519.PublicKey - Coords []uint64 - Port uint64 - Remote string RXBytes uint64 TXBytes uint64 Uptime time.Duration } -type DHTEntry struct { - Key ed25519.PublicKey - Port uint64 - Rest uint64 -} - -type PathEntry struct { - Key ed25519.PublicKey - Path []uint64 -} - -type Session struct { - Key ed25519.PublicKey -} - -func (c *Core) GetSelf() Self { - var self Self +func (c *Core) GetSelf() SelfInfo { + var self SelfInfo s := c.PacketConn.PacketConn.Debug.GetSelf() self.Key = s.Key - self.Root = s.Root - self.Coords = s.Coords + self.RoutingEntries = s.RoutingEntries return self } -func (c *Core) GetPeers() []Peer { - var peers []Peer - names := make(map[net.Conn]string) - c.links.mutex.Lock() - for _, info := range c.links.links { - names[info.conn] = info.lname +func (c *Core) GetPeers() []PeerInfo { + peers := []PeerInfo{} + conns := map[net.Conn]network.DebugPeerInfo{} + iwpeers := c.PacketConn.PacketConn.Debug.GetPeers() + for _, p := range iwpeers { + conns[p.Conn] = p } - c.links.mutex.Unlock() - ps := c.PacketConn.PacketConn.Debug.GetPeers() - for _, p := range ps { - var info Peer - info.Key = p.Key - info.Root = p.Root - info.Coords = p.Coords - info.Port = p.Port - info.Remote = p.Conn.RemoteAddr().String() - if name := names[p.Conn]; name != "" { - info.Remote = name + + phony.Block(&c.links, func() { + for info, state := range c.links._links { + var peerinfo PeerInfo + var conn net.Conn + peerinfo.URI = info.uri + peerinfo.LastError = state._err + peerinfo.LastErrorTime = state._errtime + if c := state._conn; c != nil { + conn = c + peerinfo.Up = true + peerinfo.Inbound = state.linkType == linkTypeIncoming + peerinfo.RXBytes = atomic.LoadUint64(&c.rx) + peerinfo.TXBytes = atomic.LoadUint64(&c.tx) + peerinfo.RXRate = atomic.LoadUint64(&c.rxrate) + peerinfo.TXRate = atomic.LoadUint64(&c.txrate) + peerinfo.Uptime = time.Since(c.up) + } + if p, ok := conns[conn]; ok { + peerinfo.Key = p.Key + peerinfo.Root = p.Root + peerinfo.Port = p.Port + peerinfo.Priority = p.Priority + peerinfo.Latency = p.Latency + peerinfo.Cost = p.Cost + } + peers = append(peers, peerinfo) } - if linkconn, ok := p.Conn.(*linkConn); ok { - info.RXBytes = atomic.LoadUint64(&linkconn.rx) - info.TXBytes = atomic.LoadUint64(&linkconn.tx) - info.Uptime = time.Since(linkconn.up) - } - peers = append(peers, info) - } + }) + return peers } -func (c *Core) GetDHT() []DHTEntry { - var dhts []DHTEntry - ds := c.PacketConn.PacketConn.Debug.GetDHT() - for _, d := range ds { - var info DHTEntry - info.Key = d.Key - info.Port = d.Port - info.Rest = d.Rest - dhts = append(dhts, info) +func (c *Core) GetTree() []TreeEntryInfo { + var trees []TreeEntryInfo + ts := c.PacketConn.PacketConn.Debug.GetTree() + for _, t := range ts { + var info TreeEntryInfo + info.Key = t.Key + info.Parent = t.Parent + info.Sequence = t.Sequence + //info.Port = d.Port + //info.Rest = d.Rest + trees = append(trees, info) } - return dhts + return trees } -func (c *Core) GetPaths() []PathEntry { - var paths []PathEntry +func (c *Core) GetPaths() []PathEntryInfo { + var paths []PathEntryInfo ps := c.PacketConn.PacketConn.Debug.GetPaths() for _, p := range ps { - var info PathEntry + var info PathEntryInfo info.Key = p.Key + info.Sequence = p.Sequence info.Path = p.Path paths = append(paths, info) } return paths } -func (c *Core) GetSessions() []Session { - var sessions []Session +func (c *Core) GetSessions() []SessionInfo { + var sessions []SessionInfo ss := c.PacketConn.Debug.GetSessions() for _, s := range ss { - var info Session + var info SessionInfo info.Key = s.Key + info.RXBytes = s.RX + info.TXBytes = s.TX + info.Uptime = s.Uptime sessions = append(sessions, info) } return sessions @@ -130,8 +153,15 @@ func (c *Core) GetSessions() []Session { // Listen starts a new listener (either TCP or TLS). The input should be a url.URL // parsed from a string of the form e.g. "tcp://a.b.c.d:e". In the case of a // link-local address, the interface should be provided as the second argument. -func (c *Core) Listen(u *url.URL, sintf string) (*TcpListener, error) { - return c.links.tcp.listenURL(u, sintf) +func (c *Core) Listen(u *url.URL, sintf string) (*Listener, error) { + return c.links.listen(u, sintf, false) +} + +// ListenLocal starts a listener, like the Listen function, but is used for +// more trustworthy situations where you want to ignore AllowedPublicKeys, i.e. +// with multicast listeners. +func (c *Core) ListenLocal(u *url.URL, sintf string) (*Listener, error) { + return c.links.listen(u, sintf, true) } // Address gets the IPv6 address of the Yggdrasil node. This is always a /128 @@ -159,92 +189,37 @@ func (c *Core) Subnet() net.IPNet { // may be useful if you want to redirect the output later. Note that this // expects a Logger from the github.com/gologme/log package and not from Go's // built-in log package. -func (c *Core) SetLogger(log *log.Logger) { +func (c *Core) SetLogger(log Logger) { c.log = log } // AddPeer adds a peer. This should be specified in the peer URI format, e.g.: -// tcp://a.b.c.d:e -// socks://a.b.c.d:e/f.g.h.i:j +// +// tcp://a.b.c.d:e +// socks://a.b.c.d:e/f.g.h.i:j +// // This adds the peer to the peer list, so that they will be called again if the // connection drops. -/* -func (c *Core) AddPeer(addr string, sintf string) error { - if err := c.CallPeer(addr, sintf); err != nil { - // TODO: We maybe want this to write the peer to the persistent - // configuration even if a connection attempt fails, but first we'll need to - // move the code to check the peer URI so that we don't deliberately save a - // peer with a known bad URI. Loading peers from config should really do the - // same thing too but I don't think that happens today - return err - } - c.config.Mutex.Lock() - defer c.config.Mutex.Unlock() - if sintf == "" { - for _, peer := range c.config.Current.Peers { - if peer == addr { - return errors.New("peer already added") - } - } - c.config.Current.Peers = append(c.config.Current.Peers, addr) - } else { - if _, ok := c.config.Current.InterfacePeers[sintf]; ok { - for _, peer := range c.config.Current.InterfacePeers[sintf] { - if peer == addr { - return errors.New("peer already added") - } - } - } - if _, ok := c.config.Current.InterfacePeers[sintf]; !ok { - c.config.Current.InterfacePeers[sintf] = []string{addr} - } else { - c.config.Current.InterfacePeers[sintf] = append(c.config.Current.InterfacePeers[sintf], addr) - } - } - return nil +func (c *Core) AddPeer(u *url.URL, sintf string) error { + return c.links.add(u, sintf, linkTypePersistent) } -*/ -/* -func (c *Core) RemovePeer(addr string, sintf string) error { - if sintf == "" { - for i, peer := range c.config.Current.Peers { - if peer == addr { - c.config.Current.Peers = append(c.config.Current.Peers[:i], c.config.Current.Peers[i+1:]...) - break - } - } - } else if _, ok := c.config.Current.InterfacePeers[sintf]; ok { - for i, peer := range c.config.Current.InterfacePeers[sintf] { - if peer == addr { - c.config.Current.InterfacePeers[sintf] = append(c.config.Current.InterfacePeers[sintf][:i], c.config.Current.InterfacePeers[sintf][i+1:]...) - break - } - } - } - - panic("TODO") // Get the net.Conn to this peer (if any) and close it - c.peers.Act(nil, func() { - ports := c.peers.ports - for _, peer := range ports { - if addr == peer.intf.name() { - c.peers._removePeer(peer) - } - } - }) - - return nil +// RemovePeer removes a peer. The peer should be specified in URI format, see AddPeer. +// The peer is not disconnected immediately. +func (c *Core) RemovePeer(u *url.URL, sintf string) error { + return c.links.remove(u, sintf, linkTypePersistent) } -*/ // CallPeer calls a peer once. This should be specified in the peer URI format, // e.g.: -// tcp://a.b.c.d:e -// socks://a.b.c.d:e/f.g.h.i:j +// +// tcp://a.b.c.d:e +// socks://a.b.c.d:e/f.g.h.i:j +// // This does not add the peer to the peer list, so if the connection drops, the // peer will not be called again automatically. func (c *Core) CallPeer(u *url.URL, sintf string) error { - return c.links.call(u, sintf) + return c.links.add(u, sintf, linkTypeEphemeral) } func (c *Core) PublicKey() ed25519.PublicKey { @@ -254,7 +229,7 @@ func (c *Core) PublicKey() ed25519.PublicKey { // Hack to get the admin stuff working, TODO something cleaner type AddHandler interface { - AddHandler(name string, args []string, handlerfunc AddHandlerFunc) error + AddHandler(name, desc string, args []string, handlerfunc AddHandlerFunc) error } type AddHandlerFunc func(json.RawMessage) (interface{}, error) @@ -262,16 +237,28 @@ type AddHandlerFunc func(json.RawMessage) (interface{}, error) // SetAdmin must be called after Init and before Start. // It sets the admin handler for NodeInfo and the Debug admin functions. func (c *Core) SetAdmin(a AddHandler) error { - if err := a.AddHandler("getNodeInfo", []string{"key"}, c.proto.nodeinfo.nodeInfoAdminHandler); err != nil { + if err := a.AddHandler( + "getNodeInfo", "Request nodeinfo from a remote node by its public key", []string{"key"}, + c.proto.nodeinfo.nodeInfoAdminHandler, + ); err != nil { return err } - if err := a.AddHandler("debug_remoteGetSelf", []string{"key"}, c.proto.getSelfHandler); err != nil { + if err := a.AddHandler( + "debug_remoteGetSelf", "Debug use only", []string{"key"}, + c.proto.getSelfHandler, + ); err != nil { return err } - if err := a.AddHandler("debug_remoteGetPeers", []string{"key"}, c.proto.getPeersHandler); err != nil { + if err := a.AddHandler( + "debug_remoteGetPeers", "Debug use only", []string{"key"}, + c.proto.getPeersHandler, + ); err != nil { return err } - if err := a.AddHandler("debug_remoteGetDHT", []string{"key"}, c.proto.getDHTHandler); err != nil { + if err := a.AddHandler( + "debug_remoteGetTree", "Debug use only", []string{"key"}, + c.proto.getTreeHandler, + ); err != nil { return err } return nil diff --git a/src/core/core.go b/src/core/core.go index 0332980b..a7f9fe96 100644 --- a/src/core/core.go +++ b/src/core/core.go @@ -3,21 +3,20 @@ package core import ( "context" "crypto/ed25519" - "encoding/hex" - "errors" + "crypto/tls" "fmt" - "io/ioutil" + "io" "net" "net/url" "time" iwe "github.com/Arceliar/ironwood/encrypted" + iwn "github.com/Arceliar/ironwood/network" iwt "github.com/Arceliar/ironwood/types" "github.com/Arceliar/phony" "github.com/gologme/log" - "github.com/yggdrasil-network/yggdrasil-go/src/config" - //"github.com/yggdrasil-network/yggdrasil-go/src/crypto" + "github.com/yggdrasil-network/yggdrasil-go/src/address" "github.com/yggdrasil-network/yggdrasil-go/src/version" ) @@ -29,110 +28,35 @@ type Core struct { // guarantee that it will be covered by the mutex phony.Inbox *iwe.PacketConn - config *config.NodeConfig // Config + ctx context.Context + cancel context.CancelFunc secret ed25519.PrivateKey public ed25519.PublicKey links links proto protoHandler - log *log.Logger + log Logger addPeerTimer *time.Timer - ctx context.Context - ctxCancel context.CancelFunc + config struct { + tls *tls.Config // immutable after startup + //_peers map[Peer]*linkInfo // configurable after startup + _listeners map[ListenAddress]struct{} // configurable after startup + peerFilter func(ip net.IP) bool // immutable after startup + nodeinfo NodeInfo // immutable after startup + nodeinfoPrivacy NodeInfoPrivacy // immutable after startup + _allowedPublicKeys map[[32]byte]struct{} // configurable after startup + } + pathNotify func(ed25519.PublicKey) } -func (c *Core) _init() error { - // TODO separate init and start functions - // Init sets up structs - // Start launches goroutines that depend on structs being set up - // This is pretty much required to completely avoid race conditions - c.config.RLock() - defer c.config.RUnlock() +func New(cert *tls.Certificate, logger Logger, opts ...SetupOption) (*Core, error) { + c := &Core{ + log: logger, + } + c.ctx, c.cancel = context.WithCancel(context.Background()) if c.log == nil { - c.log = log.New(ioutil.Discard, "", 0) + c.log = log.New(io.Discard, "", 0) } - sigPriv, err := hex.DecodeString(c.config.PrivateKey) - if err != nil { - return err - } - if len(sigPriv) < ed25519.PrivateKeySize { - return errors.New("PrivateKey is incorrect length") - } - - c.secret = ed25519.PrivateKey(sigPriv) - c.public = c.secret.Public().(ed25519.PublicKey) - // TODO check public against current.PublicKey, error if they don't match - - c.PacketConn, err = iwe.NewPacketConn(c.secret) - c.ctx, c.ctxCancel = context.WithCancel(context.Background()) - c.proto.init(c) - if err := c.proto.nodeinfo.setNodeInfo(c.config.NodeInfo, c.config.NodeInfoPrivacy); err != nil { - return fmt.Errorf("setNodeInfo: %w", err) - } - return err -} - -// If any static peers were provided in the configuration above then we should -// configure them. The loop ensures that disconnected peers will eventually -// be reconnected with. -func (c *Core) _addPeerLoop() { - c.config.RLock() - defer c.config.RUnlock() - - if c.addPeerTimer == nil { - return - } - - // Add peers from the Peers section - for _, peer := range c.config.Peers { - go func(peer string, intf string) { - u, err := url.Parse(peer) - if err != nil { - c.log.Errorln("Failed to parse peer url:", peer, err) - } - if err := c.CallPeer(u, intf); err != nil { - c.log.Errorln("Failed to add peer:", err) - } - }(peer, "") // TODO: this should be acted and not in a goroutine? - } - - // Add peers from the InterfacePeers section - for intf, intfpeers := range c.config.InterfacePeers { - for _, peer := range intfpeers { - go func(peer string, intf string) { - u, err := url.Parse(peer) - if err != nil { - c.log.Errorln("Failed to parse peer url:", peer, err) - } - if err := c.CallPeer(u, intf); err != nil { - c.log.Errorln("Failed to add peer:", err) - } - }(peer, intf) // TODO: this should be acted and not in a goroutine? - } - } - - c.addPeerTimer = time.AfterFunc(time.Minute, func() { - c.Act(nil, c._addPeerLoop) - }) -} - -// Start starts up Yggdrasil using the provided config.NodeConfig, and outputs -// debug logging through the provided log.Logger. The started stack will include -// TCP and UDP sockets, a multicast discovery socket, an admin socket, router, -// switch and DHT node. A config.NodeState is returned which contains both the -// current and previous configurations (from reconfigures). -func (c *Core) Start(nc *config.NodeConfig, log *log.Logger) (err error) { - phony.Block(c, func() { - err = c._start(nc, log) - }) - return -} - -// This function is unsafe and should only be ran by the core actor. -func (c *Core) _start(nc *config.NodeConfig, log *log.Logger) error { - c.log = log - c.config = nc - if name := version.BuildName(); name != "unknown" { c.log.Infoln("Build name:", name) } @@ -140,61 +64,121 @@ func (c *Core) _start(nc *config.NodeConfig, log *log.Logger) error { c.log.Infoln("Build version:", version) } - c.log.Infoln("Starting up...") - if err := c._init(); err != nil { - c.log.Errorln("Failed to initialize core") - return err + var err error + c.config._listeners = map[ListenAddress]struct{}{} + c.config._allowedPublicKeys = map[[32]byte]struct{}{} + for _, opt := range opts { + switch opt.(type) { + case Peer, ListenAddress: + // We can't do peers yet as the links aren't set up. + continue + default: + if err = c._applyOption(opt); err != nil { + return nil, fmt.Errorf("failed to apply configuration option %T: %w", opt, err) + } + } } + if cert == nil || cert.PrivateKey == nil { + return nil, fmt.Errorf("no private key supplied") + } + var ok bool + if c.secret, ok = cert.PrivateKey.(ed25519.PrivateKey); !ok { + return nil, fmt.Errorf("private key must be ed25519") + } + if len(c.secret) != ed25519.PrivateKeySize { + return nil, fmt.Errorf("private key is incorrect length") + } + c.public = c.secret.Public().(ed25519.PublicKey) + if c.config.tls, err = c.generateTLSConfig(cert); err != nil { + return nil, fmt.Errorf("error generating TLS config: %w", err) + } + keyXform := func(key ed25519.PublicKey) ed25519.PublicKey { + return address.SubnetForKey(key).GetKey() + } + if c.PacketConn, err = iwe.NewPacketConn( + c.secret, + iwn.WithBloomTransform(keyXform), + iwn.WithPeerMaxMessageSize(65535*2), + iwn.WithPathNotify(c.doPathNotify), + ); err != nil { + return nil, fmt.Errorf("error creating encryption: %w", err) + } + c.proto.init(c) if err := c.links.init(c); err != nil { - c.log.Errorln("Failed to start link interfaces") - return err + return nil, fmt.Errorf("error initialising links: %w", err) } + for _, opt := range opts { + switch opt.(type) { + case Peer, ListenAddress: + // Now do the peers and listeners. + if err = c._applyOption(opt); err != nil { + return nil, fmt.Errorf("failed to apply configuration option %T: %w", opt, err) + } + default: + continue + } + } + if err := c.proto.nodeinfo.setNodeInfo(c.config.nodeinfo, bool(c.config.nodeinfoPrivacy)); err != nil { + return nil, fmt.Errorf("error setting node info: %w", err) + } + for listenaddr := range c.config._listeners { + u, err := url.Parse(string(listenaddr)) + if err != nil { + c.log.Errorf("Invalid listener URI %q specified, ignoring\n", listenaddr) + continue + } + if _, err = c.links.listen(u, "", false); err != nil { + c.log.Errorf("Failed to start listener %q: %s\n", listenaddr, err) + } + } + return c, nil +} - c.addPeerTimer = time.AfterFunc(0, func() { - c.Act(nil, c._addPeerLoop) +func (c *Core) RetryPeersNow() { + phony.Block(&c.links, func() { + for _, l := range c.links._links { + select { + case l.kick <- struct{}{}: + default: + } + } }) - - c.log.Infoln("Startup complete") - return nil } // Stop shuts down the Yggdrasil node. func (c *Core) Stop() { phony.Block(c, func() { c.log.Infoln("Stopping...") - c._close() + _ = c._close() c.log.Infoln("Stopped") }) } -func (c *Core) Close() error { - var err error - phony.Block(c, func() { - err = c._close() - }) - return err -} - // This function is unsafe and should only be ran by the core actor. func (c *Core) _close() error { - c.ctxCancel() + c.cancel() + c.links.shutdown() err := c.PacketConn.Close() if c.addPeerTimer != nil { c.addPeerTimer.Stop() c.addPeerTimer = nil } - _ = c.links.stop() return err } func (c *Core) MTU() uint64 { const sessionTypeOverhead = 1 - return c.PacketConn.MTU() - sessionTypeOverhead + MTU := c.PacketConn.MTU() - sessionTypeOverhead + if MTU > 65535 { + MTU = 65535 + } + return MTU } func (c *Core) ReadFrom(p []byte) (n int, from net.Addr, err error) { - buf := make([]byte, c.PacketConn.MTU(), 65535) + buf := allocBytes(int(c.PacketConn.MTU())) + defer freeBytes(buf) for { bs := buf n, from, err = c.PacketConn.ReadFrom(bs) @@ -228,7 +212,8 @@ func (c *Core) ReadFrom(p []byte) (n int, from net.Addr, err error) { } func (c *Core) WriteTo(p []byte, addr net.Addr) (n int, err error) { - buf := make([]byte, 0, 65535) + buf := allocBytes(0) + defer func() { freeBytes(buf) }() buf = append(buf, typeSessionTraffic) buf = append(buf, p...) n, err = c.PacketConn.WriteTo(buf, addr) @@ -237,3 +222,31 @@ func (c *Core) WriteTo(p []byte, addr net.Addr) (n int, err error) { } return } + +func (c *Core) doPathNotify(key ed25519.PublicKey) { + c.Act(nil, func() { + if c.pathNotify != nil { + c.pathNotify(key) + } + }) +} + +func (c *Core) SetPathNotify(notify func(ed25519.PublicKey)) { + c.Act(nil, func() { + c.pathNotify = notify + }) +} + +type Logger interface { + Printf(string, ...interface{}) + Println(...interface{}) + Infof(string, ...interface{}) + Infoln(...interface{}) + Warnf(string, ...interface{}) + Warnln(...interface{}) + Errorf(string, ...interface{}) + Errorln(...interface{}) + Debugf(string, ...interface{}) + Debugln(...interface{}) + Traceln(...interface{}) +} diff --git a/src/core/core_test.go b/src/core/core_test.go index fcfe2e31..f186f43f 100644 --- a/src/core/core_test.go +++ b/src/core/core_test.go @@ -2,28 +2,16 @@ package core import ( "bytes" - "math/rand" + "crypto/rand" "net/url" "os" "testing" "time" "github.com/gologme/log" - "github.com/yggdrasil-network/yggdrasil-go/src/config" - "github.com/yggdrasil-network/yggdrasil-go/src/defaults" ) -// GenerateConfig produces default configuration with suitable modifications for tests. -func GenerateConfig() *config.NodeConfig { - cfg := defaults.GenerateConfig() - cfg.AdminListen = "none" - cfg.Listen = []string{"tcp://127.0.0.1:0"} - cfg.IfName = "none" - - return cfg -} - // GetLoggerWithPrefix creates a new logger instance with prefix. // If verbose is set to true, three log levels are enabled: "info", "warn", "error". func GetLoggerWithPrefix(prefix string, verbose bool) *log.Logger { @@ -37,27 +25,65 @@ func GetLoggerWithPrefix(prefix string, verbose bool) *log.Logger { return l } +func require_NoError(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatal(err) + } +} + +func require_Equal[T comparable](t *testing.T, a, b T) { + t.Helper() + if a != b { + t.Fatalf("%v != %v", a, b) + } +} + +func require_True(t *testing.T, a bool) { + t.Helper() + if !a { + t.Fatal("expected true") + } +} + // CreateAndConnectTwo creates two nodes. nodeB connects to nodeA. // Verbosity flag is passed to logger. func CreateAndConnectTwo(t testing.TB, verbose bool) (nodeA *Core, nodeB *Core) { - nodeA = new(Core) - if err := nodeA.Start(GenerateConfig(), GetLoggerWithPrefix("A: ", verbose)); err != nil { + var err error + + cfgA, cfgB := config.GenerateConfig(), config.GenerateConfig() + if err = cfgA.GenerateSelfSignedCertificate(); err != nil { + t.Fatal(err) + } + if err = cfgB.GenerateSelfSignedCertificate(); err != nil { t.Fatal(err) } - nodeB = new(Core) - if err := nodeB.Start(GenerateConfig(), GetLoggerWithPrefix("B: ", verbose)); err != nil { + logger := GetLoggerWithPrefix("", false) + logger.EnableLevel("debug") + + if nodeA, err = New(cfgA.Certificate, logger); err != nil { + t.Fatal(err) + } + if nodeB, err = New(cfgB.Certificate, logger); err != nil { t.Fatal(err) } - u, err := url.Parse("tcp://" + nodeA.links.tcp.getAddr().String()) + nodeAListenURL, err := url.Parse("tcp://localhost:0") if err != nil { t.Fatal(err) } - err = nodeB.CallPeer(u, "") + nodeAListener, err := nodeA.Listen(nodeAListenURL, "") if err != nil { t.Fatal(err) } + nodeAURL, err := url.Parse("tcp://" + nodeAListener.Addr().String()) + if err != nil { + t.Fatal(err) + } + if err = nodeB.CallPeer(nodeAURL, ""); err != nil { + t.Fatal(err) + } time.Sleep(100 * time.Millisecond) @@ -76,7 +102,13 @@ func WaitConnected(nodeA, nodeB *Core) bool { // It may take up to 3 seconds, but let's wait 5. for i := 0; i < 50; i++ { time.Sleep(100 * time.Millisecond) - if len(nodeA.GetPeers()) > 0 && len(nodeB.GetPeers()) > 0 { + /* + if len(nodeA.GetPeers()) > 0 && len(nodeB.GetPeers()) > 0 { + return true + } + */ + if len(nodeA.GetTree()) > 1 && len(nodeB.GetTree()) > 1 { + time.Sleep(3 * time.Second) // FIXME hack, there's still stuff happening internally return true } } @@ -135,7 +167,7 @@ func TestCore_Start_Transfer(t *testing.T) { // Send msg := make([]byte, msgLen) - rand.Read(msg[40:]) + _, _ = rand.Read(msg[40:]) msg[0] = 0x60 copy(msg[8:24], nodeB.Address()) copy(msg[24:40], nodeA.Address()) @@ -167,7 +199,7 @@ func BenchmarkCore_Start_Transfer(b *testing.B) { // Send msg := make([]byte, msgLen) - rand.Read(msg[40:]) + _, _ = rand.Read(msg[40:]) msg[0] = 0x60 copy(msg[8:24], nodeB.Address()) copy(msg[24:40], nodeA.Address()) @@ -190,3 +222,69 @@ func BenchmarkCore_Start_Transfer(b *testing.B) { } <-done } + +func TestAllowedPublicKeys(t *testing.T) { + logger := GetLoggerWithPrefix("", false) + cfgA, cfgB := config.GenerateConfig(), config.GenerateConfig() + require_NoError(t, cfgA.GenerateSelfSignedCertificate()) + require_NoError(t, cfgB.GenerateSelfSignedCertificate()) + + nodeA, err := New(cfgA.Certificate, logger, AllowedPublicKey("abcdef")) + require_NoError(t, err) + defer nodeA.Stop() + + nodeB, err := New(cfgB.Certificate, logger) + require_NoError(t, err) + defer nodeB.Stop() + + u, err := url.Parse("tcp://localhost:0") + require_NoError(t, err) + + l, err := nodeA.Listen(u, "") + require_NoError(t, err) + + u, err = url.Parse("tcp://" + l.Addr().String()) + require_NoError(t, err) + + require_NoError(t, nodeB.AddPeer(u, "")) + + time.Sleep(time.Second) + + peers := nodeB.GetPeers() + require_Equal(t, len(peers), 1) + require_True(t, !peers[0].Up) + require_True(t, peers[0].LastError != nil) +} + +func TestAllowedPublicKeysLocal(t *testing.T) { + logger := GetLoggerWithPrefix("", false) + cfgA, cfgB := config.GenerateConfig(), config.GenerateConfig() + require_NoError(t, cfgA.GenerateSelfSignedCertificate()) + require_NoError(t, cfgB.GenerateSelfSignedCertificate()) + + nodeA, err := New(cfgA.Certificate, logger, AllowedPublicKey("abcdef")) + require_NoError(t, err) + defer nodeA.Stop() + + nodeB, err := New(cfgB.Certificate, logger) + require_NoError(t, err) + defer nodeB.Stop() + + u, err := url.Parse("tcp://localhost:0") + require_NoError(t, err) + + l, err := nodeA.ListenLocal(u, "") + require_NoError(t, err) + + u, err = url.Parse("tcp://" + l.Addr().String()) + require_NoError(t, err) + + require_NoError(t, nodeB.AddPeer(u, "")) + + time.Sleep(time.Second) + + peers := nodeB.GetPeers() + require_Equal(t, len(peers), 1) + require_True(t, peers[0].Up) + require_True(t, peers[0].LastError == nil) +} diff --git a/src/core/debug.go b/src/core/debug.go index ee1f1ed8..354c2ecd 100644 --- a/src/core/debug.go +++ b/src/core/debug.go @@ -1,36 +1,19 @@ -//go:build debug -// +build debug - package core import ( "fmt" - "net/http" _ "net/http/pprof" "os" - "runtime" - - "github.com/gologme/log" ) -// Start the profiler in debug builds, if the required environment variable is set. +// Start the profiler if the required environment variable is set. func init() { envVarName := "PPROFLISTEN" - hostPort := os.Getenv(envVarName) - switch { - case hostPort == "": - fmt.Fprintf(os.Stderr, "DEBUG: %s not set, profiler not started.\n", envVarName) - default: + if hostPort := os.Getenv(envVarName); hostPort != "" { fmt.Fprintf(os.Stderr, "DEBUG: Starting pprof on %s\n", hostPort) - go func() { fmt.Println(http.ListenAndServe(hostPort, nil)) }() + go func() { + fmt.Fprintf(os.Stderr, "DEBUG: %s", http.ListenAndServe(hostPort, nil)) + }() } } - -// Starts the function profiler. This is only supported when built with -// '-tags build'. -func StartProfiler(log *log.Logger) error { - runtime.SetBlockProfileRate(1) - go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() - return nil -} diff --git a/src/core/link.go b/src/core/link.go index f96c9be9..f30016f9 100644 --- a/src/core/link.go +++ b/src/core/link.go @@ -1,289 +1,756 @@ package core import ( - "crypto/ed25519" + "bytes" + "context" "encoding/hex" "errors" "fmt" "io" "net" "net/url" + "strconv" "strings" - "sync" - - //"sync/atomic" + "sync/atomic" "time" - "sync/atomic" - + "github.com/Arceliar/phony" "github.com/yggdrasil-network/yggdrasil-go/src/address" - "github.com/yggdrasil-network/yggdrasil-go/src/util" - "golang.org/x/net/proxy" - //"github.com/Arceliar/phony" // TODO? use instead of mutexes + "golang.org/x/crypto/blake2b" ) +type linkType int + +const ( + linkTypePersistent linkType = iota // Statically configured + linkTypeEphemeral // Multicast discovered + linkTypeIncoming // Incoming connection +) + +const defaultBackoffLimit = time.Second << 12 // 1h8m16s +const minimumBackoffLimit = time.Second * 30 + type links struct { - core *Core - mutex sync.RWMutex // protects links below - links map[linkInfo]*link - tcp tcp // TCP interface support - stopped chan struct{} - // TODO timeout (to remove from switch), read from config.ReadTimeout + phony.Inbox + core *Core + tcp *linkTCP // TCP interface support + tls *linkTLS // TLS interface support + unix *linkUNIX // UNIX interface support + socks *linkSOCKS // SOCKS interface support + quic *linkQUIC // QUIC interface support + ws *linkWS // WS interface support + wss *linkWSS // WSS interface support + // _links can only be modified safely from within the links actor + _links map[linkInfo]*link // *link is nil if connection in progress + _listeners map[*Listener]context.CancelFunc +} + +type linkProtocol interface { + dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) + listen(ctx context.Context, url *url.URL, sintf string) (net.Listener, error) } // linkInfo is used as a map key type linkInfo struct { - key keyArray - linkType string // Type of link, e.g. TCP, AWDL - local string // Local name or address - remote string // Remote name or address + uri string // Peering URI in complete form + sintf string // Peering source interface (i.e. from InterfacePeers) } +// link tracks the state of a connection, either persistent or non-persistent type link struct { - lname string - links *links - conn *linkConn - options linkOptions - info linkInfo - incoming bool - force bool - closed chan struct{} + ctx context.Context // Connection context + cancel context.CancelFunc // Stop future redial attempts (when peer removed) + kick chan struct{} // Attempt to reconnect now, if backing off + linkType linkType // Type of link, i.e. outbound/inbound, persistent/ephemeral + linkProto string // Protocol carrier of link, e.g. TCP, AWDL + // The remaining fields can only be modified safely from within the links actor + _conn *linkConn // Connected link, if any, nil if not connected + _err error // Last error on the connection, if any + _errtime time.Time // Last time an error occurred } type linkOptions struct { pinnedEd25519Keys map[keyArray]struct{} + priority uint8 + tlsSNI string + password []byte + maxBackoff time.Duration +} + +type Listener struct { + listener net.Listener + ctx context.Context + Cancel context.CancelFunc +} + +func (l *Listener) Addr() net.Addr { + return l.listener.Addr() } func (l *links) init(c *Core) error { l.core = c - l.mutex.Lock() - l.links = make(map[linkInfo]*link) - l.mutex.Unlock() - l.stopped = make(chan struct{}) - - if err := l.tcp.init(l); err != nil { - c.log.Errorln("Failed to start TCP interface") - return err - } + l.tcp = l.newLinkTCP() + l.tls = l.newLinkTLS(l.tcp) + l.unix = l.newLinkUNIX() + l.socks = l.newLinkSOCKS() + l.quic = l.newLinkQUIC() + l.ws = l.newLinkWS() + l.wss = l.newLinkWSS() + l._links = make(map[linkInfo]*link) + l._listeners = make(map[*Listener]context.CancelFunc) + l.Act(nil, l._updateAverages) return nil } -func (l *links) call(u *url.URL, sintf string) error { - //u, err := url.Parse(uri) - //if err != nil { - // return fmt.Errorf("peer %s is not correctly formatted (%s)", uri, err) - //} - tcpOpts := tcpOptions{} - if pubkeys, ok := u.Query()["key"]; ok && len(pubkeys) > 0 { - tcpOpts.pinnedEd25519Keys = make(map[keyArray]struct{}) - for _, pubkey := range pubkeys { - if sigPub, err := hex.DecodeString(pubkey); err == nil { - var sigPubKey keyArray - copy(sigPubKey[:], sigPub) - tcpOpts.pinnedEd25519Keys[sigPubKey] = struct{}{} +func (l *links) _updateAverages() { + select { + case <-l.core.ctx.Done(): + return + default: + } + + for _, l := range l._links { + if l._conn == nil { + continue + } + rx := atomic.LoadUint64(&l._conn.rx) + tx := atomic.LoadUint64(&l._conn.tx) + lastrx := atomic.LoadUint64(&l._conn.lastrx) + lasttx := atomic.LoadUint64(&l._conn.lasttx) + atomic.StoreUint64(&l._conn.rxrate, rx-lastrx) + atomic.StoreUint64(&l._conn.txrate, tx-lasttx) + atomic.StoreUint64(&l._conn.lastrx, rx) + atomic.StoreUint64(&l._conn.lasttx, tx) + } + + time.AfterFunc(time.Second, func() { + l.Act(nil, l._updateAverages) + }) +} + +func (l *links) shutdown() { + phony.Block(l, func() { + for _, cancel := range l._listeners { + cancel() + } + for _, link := range l._links { + if link._conn != nil { + _ = link._conn.Close() } } - } - switch u.Scheme { - case "tcp": - l.tcp.call(u.Host, tcpOpts, sintf) - case "socks": - tcpOpts.socksProxyAddr = u.Host - if u.User != nil { - tcpOpts.socksProxyAuth = &proxy.Auth{} - tcpOpts.socksProxyAuth.User = u.User.Username() - tcpOpts.socksProxyAuth.Password, _ = u.User.Password() + }) +} + +type linkError string + +func (e linkError) Error() string { return string(e) } + +const ErrLinkAlreadyConfigured = linkError("peer is already configured") +const ErrLinkNotConfigured = linkError("peer is not configured") +const ErrLinkPriorityInvalid = linkError("priority value is invalid") +const ErrLinkPinnedKeyInvalid = linkError("pinned public key is invalid") +const ErrLinkPasswordInvalid = linkError("invalid password supplied") +const ErrLinkUnrecognisedSchema = linkError("link schema unknown") +const ErrLinkMaxBackoffInvalid = linkError("max backoff duration invalid") +const ErrLinkSNINotSupported = linkError("SNI not supported on this link type") +const ErrLinkNoSuitableIPs = linkError("peer has no suitable addresses") + +func (l *links) add(u *url.URL, sintf string, linkType linkType) error { + var retErr error + phony.Block(l, func() { + // Generate the link info and see whether we think we already + // have an open peering to this peer. + lu := urlForLinkInfo(*u) + info := linkInfo{ + uri: lu.String(), + sintf: sintf, + } + + // Collect together the link options, these are global options + // that are not specific to any given protocol. + options := linkOptions{ + maxBackoff: defaultBackoffLimit, + } + for _, pubkey := range u.Query()["key"] { + sigPub, err := hex.DecodeString(pubkey) + if err != nil { + retErr = ErrLinkPinnedKeyInvalid + return + } + var sigPubKey keyArray + copy(sigPubKey[:], sigPub) + if options.pinnedEd25519Keys == nil { + options.pinnedEd25519Keys = map[keyArray]struct{}{} + } + options.pinnedEd25519Keys[sigPubKey] = struct{}{} + } + if p := u.Query().Get("priority"); p != "" { + pi, err := strconv.ParseUint(p, 10, 8) + if err != nil { + retErr = ErrLinkPriorityInvalid + return + } + options.priority = uint8(pi) + } + if p := u.Query().Get("password"); p != "" { + if len(p) > blake2b.Size { + retErr = ErrLinkPasswordInvalid + return + } + options.password = []byte(p) + } + if p := u.Query().Get("maxbackoff"); p != "" { + d, err := time.ParseDuration(p) + if err != nil || d < minimumBackoffLimit { + retErr = ErrLinkMaxBackoffInvalid + return + } + options.maxBackoff = d } - tcpOpts.upgrade = l.tcp.tls.forDialer // TODO make this configurable - pathtokens := strings.Split(strings.Trim(u.Path, "/"), "/") - l.tcp.call(pathtokens[0], tcpOpts, sintf) - case "tls": - tcpOpts.upgrade = l.tcp.tls.forDialer // SNI headers must contain hostnames and not IP addresses, so we must make sure // that we do not populate the SNI with an IP literal. We do this by splitting // the host-port combo from the query option and then seeing if it parses to an // IP address successfully or not. if sni := u.Query().Get("sni"); sni != "" { if net.ParseIP(sni) == nil { - tcpOpts.tlsSNI = sni + options.tlsSNI = sni } } // If the SNI is not configured still because the above failed then we'll try // again but this time we'll use the host part of the peering URI instead. - if tcpOpts.tlsSNI == "" { + if options.tlsSNI == "" { if host, _, err := net.SplitHostPort(u.Host); err == nil && net.ParseIP(host) == nil { - tcpOpts.tlsSNI = host + options.tlsSNI = host } } - l.tcp.call(u.Host, tcpOpts, sintf) + + // If we think we're already connected to this peer, load up + // the existing peer state. Try to kick the peer if possible, + // which will cause an immediate connection attempt if it is + // backing off for some reason. + state, ok := l._links[info] + if ok && state != nil { + select { + case state.kick <- struct{}{}: + default: + } + retErr = ErrLinkAlreadyConfigured + return + } + + // Create the link entry. This will contain the connection + // in progress (if any), any error details and a context that + // lets the link be cancelled later. + state = &link{ + linkType: linkType, + linkProto: strings.ToUpper(u.Scheme), + kick: make(chan struct{}), + } + state.ctx, state.cancel = context.WithCancel(l.core.ctx) + + // Store the state of the link so that it can be queried later. + l._links[info] = state + + // Track how many consecutive connection failures we have had, + // as we will back off exponentially rather than hammering the + // remote node endlessly. + var backoff int + + // backoffNow is called when there's a connection error. It + // will wait for the specified amount of time and then return + // true, unless the peering context was cancelled (due to a + // peer removal most likely), in which case it returns false. + // The caller should check the return value to decide whether + // or not to give up trying. + backoffNow := func() bool { + if backoff < 32 { + backoff++ + } + duration := time.Second << backoff + if duration > options.maxBackoff { + duration = options.maxBackoff + } + select { + case <-state.kick: + return true + case <-state.ctx.Done(): + return false + case <-l.core.ctx.Done(): + return false + case <-time.After(duration): + return true + } + } + + // resetBackoff is called by the connection handler when the + // handshake has successfully completed. + resetBackoff := func() { + backoff = 0 + } + + // The goroutine is responsible for attempting the connection + // and then running the handler. If the connection is persistent + // then the loop will run endlessly, using backoffs as needed. + // Otherwise the loop will end, cleaning up the link entry. + go func() { + defer phony.Block(l, func() { + if l._links[info] == state { + delete(l._links, info) + } + }) + + // This loop will run each and every time we want to attempt + // a connection to this peer. + // TODO get rid of this loop, this is *exactly* what time.AfterFunc is for, we should just send a signal to the links actor to kick off a goroutine as needed + for { + select { + case <-state.ctx.Done(): + // The peering context has been cancelled, so don't try + // to dial again. + return + default: + } + + conn, err := l.connect(state.ctx, u, info, options) + if err != nil || conn == nil { + if err == nil && conn == nil { + l.core.log.Warnf("Link %q reached inconsistent error state", u.String()) + } + if linkType == linkTypePersistent { + // If the link is a persistent configured peering, + // store information about the connection error so + // that we can report it through the admin socket. + phony.Block(l, func() { + state._conn = nil + state._err = err + state._errtime = time.Now() + }) + + // Back off for a bit. If true is returned here, we + // can continue onto the next loop iteration to try + // the next connection. + if backoffNow() { + continue + } + return + } + // Ephemeral and incoming connections don't remain + // after a connection failure, so exit out of the + // loop and clean up the link entry. + break + } + + // The linkConn wrapper allows us to track the number of + // bytes written to and read from this connection without + // the help of ironwood. + lc := &linkConn{ + Conn: conn, + up: time.Now(), + } + + // Update the link state with our newly wrapped connection. + // Clear the error state. + var doRet bool + phony.Block(l, func() { + if state._conn != nil { + // If a peering has come up in this time, abort this one. + doRet = true + } + state._conn = lc + }) + if doRet { + return + } + + // Give the connection to the handler. The handler will block + // for the lifetime of the connection. + switch err = l.handler(linkType, options, lc, resetBackoff, false); { + case err == nil: + case errors.Is(err, io.EOF): + case errors.Is(err, net.ErrClosed): + default: + l.core.log.Debugf("Link %s error: %s\n", u.Host, err) + } + + // The handler has stopped running so the connection is dead, + // try to close the underlying socket just in case and then + // update the link state. + _ = lc.Close() + phony.Block(l, func() { + state._conn = nil + if err == nil { + err = fmt.Errorf("remote side closed the connection") + } + state._err = err + state._errtime = time.Now() + }) + + // If the link is persistently configured, back off if needed + // and then try reconnecting. Otherwise, exit out. + if linkType == linkTypePersistent { + if backoffNow() { + continue + } + } + // Ephemeral or incoming connections don't reconnect. + return + } + }() + }) + return retErr +} + +func (l *links) remove(u *url.URL, sintf string, _ linkType) error { + var retErr error + phony.Block(l, func() { + // Generate the link info and see whether we think we already + // have an open peering to this peer. + lu := urlForLinkInfo(*u) + info := linkInfo{ + uri: lu.String(), + sintf: sintf, + } + + // If this peer is already configured then we will close the + // connection and stop it from retrying. + state, ok := l._links[info] + if ok && state != nil { + state.cancel() + if conn := state._conn; conn != nil { + retErr = conn.Close() + } + return + } + + retErr = ErrLinkNotConfigured + }) + return retErr +} + +func (l *links) listen(u *url.URL, sintf string, local bool) (*Listener, error) { + ctx, ctxcancel := context.WithCancel(l.core.ctx) + var protocol linkProtocol + switch strings.ToLower(u.Scheme) { + case "tcp": + protocol = l.tcp + case "tls": + protocol = l.tls + case "unix": + protocol = l.unix + case "quic": + protocol = l.quic + case "ws": + protocol = l.ws + case "wss": + protocol = l.wss default: - return errors.New("unknown call scheme: " + u.Scheme) + ctxcancel() + return nil, ErrLinkUnrecognisedSchema } - return nil + listener, err := protocol.listen(ctx, u, sintf) + if err != nil { + ctxcancel() + return nil, err + } + addr := listener.Addr() + cancel := func() { + ctxcancel() + if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) { + l.core.log.Warnf("Error closing %s listener %s: %s", strings.ToUpper(u.Scheme), addr, err) + } + } + li := &Listener{ + listener: listener, + ctx: ctx, + Cancel: cancel, + } + + var options linkOptions + if p := u.Query().Get("priority"); p != "" { + pi, err := strconv.ParseUint(p, 10, 8) + if err != nil { + return nil, ErrLinkPriorityInvalid + } + options.priority = uint8(pi) + } + if p := u.Query().Get("password"); p != "" { + if len(p) > blake2b.Size { + return nil, ErrLinkPasswordInvalid + } + options.password = []byte(p) + } + + phony.Block(l, func() { + l._listeners[li] = cancel + }) + + go func() { + l.core.log.Infof("%s listener started on %s", strings.ToUpper(u.Scheme), addr) + defer phony.Block(l, func() { + cancel() + delete(l._listeners, li) + l.core.log.Infof("%s listener stopped on %s", strings.ToUpper(u.Scheme), addr) + }) + for { + conn, err := li.listener.Accept() + if err != nil { + return + } + go func(conn net.Conn) { + defer conn.Close() + + // In order to populate a somewhat sane looking connection + // URI in the admin socket, we need to replace the host in + // the listener URL with the remote address. + pu := *u + pu.Host = conn.RemoteAddr().String() + lu := urlForLinkInfo(pu) + info := linkInfo{ + uri: lu.String(), + sintf: sintf, + } + + // If there's an existing link state for this link, get it. + // If this node is already connected to us, just drop the + // connection. This prevents duplicate peerings. + var lc *linkConn + var state *link + phony.Block(l, func() { + var ok bool + state, ok = l._links[info] + if !ok || state == nil { + state = &link{ + linkType: linkTypeIncoming, + linkProto: strings.ToUpper(u.Scheme), + kick: make(chan struct{}), + } + } + if state._conn != nil { + // If a connection has come up in this time, abort + // this one. + return + } + + // The linkConn wrapper allows us to track the number of + // bytes written to and read from this connection without + // the help of ironwood. + lc = &linkConn{ + Conn: conn, + up: time.Now(), + } + + // Update the link state with our newly wrapped connection. + // Clear the error state. + state._conn = lc + state._err = nil + state._errtime = time.Time{} + + // Store the state of the link so that it can be queried later. + l._links[info] = state + }) + defer phony.Block(l, func() { + if l._links[info] == state { + delete(l._links, info) + } + }) + if lc == nil { + return + } + + // Give the connection to the handler. The handler will block + // for the lifetime of the connection. + switch err = l.handler(linkTypeIncoming, options, lc, nil, local); { + case err == nil: + case errors.Is(err, io.EOF): + case errors.Is(err, net.ErrClosed): + default: + l.core.log.Debugf("Link %s error: %s\n", u.Host, err) + } + + // The handler has stopped running so the connection is dead, + // try to close the underlying socket just in case and then + // drop the link state. + _ = lc.Close() + }(conn) + } + }() + return li, nil } -func (l *links) create(conn net.Conn, name, linkType, local, remote string, incoming, force bool, options linkOptions) (*link, error) { - // Technically anything unique would work for names, but let's pick something human readable, just for debugging - intf := link{ - conn: &linkConn{ - Conn: conn, - up: time.Now(), - }, - lname: name, - links: l, - options: options, - info: linkInfo{ - linkType: linkType, - local: local, - remote: remote, - }, - incoming: incoming, - force: force, +func (l *links) connect(ctx context.Context, u *url.URL, info linkInfo, options linkOptions) (net.Conn, error) { + var dialer linkProtocol + switch strings.ToLower(u.Scheme) { + case "tcp": + dialer = l.tcp + case "tls": + dialer = l.tls + case "socks", "sockstls": + dialer = l.socks + case "unix": + dialer = l.unix + case "quic": + dialer = l.quic + case "ws": + dialer = l.ws + case "wss": + dialer = l.wss + default: + return nil, ErrLinkUnrecognisedSchema } - return &intf, nil + return dialer.dial(ctx, u, info, options) } -func (l *links) stop() error { - close(l.stopped) - if err := l.tcp.stop(); err != nil { - return err - } - return nil -} - -func (intf *link) handler() (chan struct{}, error) { - // TODO split some of this into shorter functions, so it's easier to read, and for the FIXME duplicate peer issue mentioned later - defer intf.conn.Close() +func (l *links) handler(linkType linkType, options linkOptions, conn net.Conn, success func(), local bool) error { meta := version_getBaseMetadata() - meta.key = intf.links.core.public - metaBytes := meta.encode() - // TODO timeouts on send/recv (goroutine for send/recv, channel select w/ timer) - var err error - if !util.FuncTimeout(30*time.Second, func() { - var n int - n, err = intf.conn.Write(metaBytes) - if err == nil && n != len(metaBytes) { - err = errors.New("incomplete metadata send") - } - }) { - return nil, errors.New("timeout on metadata send") - } + meta.publicKey = l.core.public + meta.priority = options.priority + metaBytes, err := meta.encode(l.core.secret, options.password) if err != nil { - return nil, err + return fmt.Errorf("failed to generate handshake: %w", err) } - if !util.FuncTimeout(30*time.Second, func() { - var n int - n, err = io.ReadFull(intf.conn, metaBytes) - if err == nil && n != len(metaBytes) { - err = errors.New("incomplete metadata recv") - } - }) { - return nil, errors.New("timeout on metadata recv") + if err := conn.SetDeadline(time.Now().Add(time.Second * 6)); err != nil { + return fmt.Errorf("failed to set handshake deadline: %w", err) } - if err != nil { - return nil, err + n, err := conn.Write(metaBytes) + switch { + case err != nil: + return fmt.Errorf("write handshake: %w", err) + case n != len(metaBytes): + return fmt.Errorf("incomplete handshake send") } meta = version_metadata{} base := version_getBaseMetadata() - if !meta.decode(metaBytes) { - return nil, errors.New("failed to decode metadata") + if err := meta.decode(conn, options.password); err != nil { + _ = conn.Close() + return err } if !meta.check() { - var connectError string - if intf.incoming { - connectError = "Rejected incoming connection" - } else { - connectError = "Failed to connect" - } - intf.links.core.log.Debugf("%s: %s is incompatible version (local %s, remote %s)", - connectError, - intf.lname, - fmt.Sprintf("%d.%d", base.ver, base.minorVer), - fmt.Sprintf("%d.%d", meta.ver, meta.minorVer), + return fmt.Errorf("remote node incompatible version (local %s, remote %s)", + fmt.Sprintf("%d.%d", base.majorVer, base.minorVer), + fmt.Sprintf("%d.%d", meta.majorVer, meta.minorVer), ) - return nil, errors.New("remote node is incompatible version") + } + if err = conn.SetDeadline(time.Time{}); err != nil { + return fmt.Errorf("failed to clear handshake deadline: %w", err) } // Check if the remote side matches the keys we expected. This is a bit of a weak // check - in future versions we really should check a signature or something like that. - if pinned := intf.options.pinnedEd25519Keys; pinned != nil { + if pinned := options.pinnedEd25519Keys; len(pinned) > 0 { var key keyArray - copy(key[:], meta.key) + copy(key[:], meta.publicKey) if _, allowed := pinned[key]; !allowed { - intf.links.core.log.Errorf("Failed to connect to node: %q sent ed25519 key that does not match pinned keys", intf.name()) - return nil, fmt.Errorf("failed to connect: host sent ed25519 key that does not match pinned keys") + return fmt.Errorf("node public key that does not match pinned keys") } } // Check if we're authorized to connect to this key / IP - intf.links.core.config.RLock() - allowed := intf.links.core.config.AllowedPublicKeys - intf.links.core.config.RUnlock() - isallowed := len(allowed) == 0 - for _, k := range allowed { - if k == hex.EncodeToString(meta.key) { // TODO: this is yuck - isallowed = true - break + if !local { + var allowed map[[32]byte]struct{} + phony.Block(l.core, func() { + allowed = l.core.config._allowedPublicKeys + }) + isallowed := len(allowed) == 0 + for k := range allowed { + if bytes.Equal(k[:], meta.publicKey) { + isallowed = true + break + } + } + if linkType == linkTypeIncoming && !isallowed { + return fmt.Errorf("node public key %q is not in AllowedPublicKeys", hex.EncodeToString(meta.publicKey)) } } - if intf.incoming && !intf.force && !isallowed { - intf.links.core.log.Warnf("%s connection from %s forbidden: AllowedEncryptionPublicKeys does not contain key %s", - strings.ToUpper(intf.info.linkType), intf.info.remote, hex.EncodeToString(meta.key)) - intf.close() - return nil, nil + + dir := "outbound" + if linkType == linkTypeIncoming { + dir = "inbound" } - // Check if we already have a link to this node - copy(intf.info.key[:], meta.key) - intf.links.mutex.Lock() - if oldIntf, isIn := intf.links.links[intf.info]; isIn { - intf.links.mutex.Unlock() - // FIXME we should really return an error and let the caller block instead - // That lets them do things like close connections on its own, avoid printing a connection message in the first place, etc. - intf.links.core.log.Debugln("DEBUG: found existing interface for", intf.name()) - return oldIntf.closed, nil - } else { - intf.closed = make(chan struct{}) - intf.links.links[intf.info] = intf - defer func() { - intf.links.mutex.Lock() - delete(intf.links.links, intf.info) - intf.links.mutex.Unlock() - close(intf.closed) - }() - intf.links.core.log.Debugln("DEBUG: registered interface for", intf.name()) + remoteAddr := net.IP(address.AddrForKey(meta.publicKey)[:]).String() + remoteStr := fmt.Sprintf("%s@%s", remoteAddr, conn.RemoteAddr()) + localStr := conn.LocalAddr() + priority := options.priority + if meta.priority > priority { + priority = meta.priority } - intf.links.mutex.Unlock() - themAddr := address.AddrForKey(ed25519.PublicKey(intf.info.key[:])) - themAddrString := net.IP(themAddr[:]).String() - themString := fmt.Sprintf("%s@%s", themAddrString, intf.info.remote) - intf.links.core.log.Infof("Connected %s: %s, source %s", - strings.ToUpper(intf.info.linkType), themString, intf.info.local) - // Run the handler - err = intf.links.core.HandleConn(ed25519.PublicKey(intf.info.key[:]), intf.conn) - // TODO don't report an error if it's just a 'use of closed network connection' + l.core.log.Infof("Connected %s: %s, source %s", + dir, remoteStr, localStr) + if success != nil { + success() + } + + err = l.core.HandleConn(meta.publicKey, conn, priority) + switch err { + case io.EOF, net.ErrClosed, nil: + l.core.log.Infof("Disconnected %s: %s, source %s", + dir, remoteStr, localStr) + default: + l.core.log.Infof("Disconnected %s: %s, source %s; error: %s", + dir, remoteStr, localStr, err) + } + return err +} + +func (l *links) findSuitableIP(url *url.URL, fn func(hostname string, ip net.IP, port int) (net.Conn, error)) (net.Conn, error) { + host, p, err := net.SplitHostPort(url.Host) if err != nil { - intf.links.core.log.Infof("Disconnected %s: %s, source %s; error: %s", - strings.ToUpper(intf.info.linkType), themString, intf.info.local, err) - } else { - intf.links.core.log.Infof("Disconnected %s: %s, source %s", - strings.ToUpper(intf.info.linkType), themString, intf.info.local) + return nil, err + } + port, err := strconv.Atoi(p) + if err != nil { + return nil, err + } + resp, err := net.LookupIP(host) + if err != nil { + return nil, err + } + var _ips [64]net.IP + ips := _ips[:0] + for _, ip := range resp { + switch { + case ip.IsUnspecified(): + continue + case ip.IsMulticast(): + continue + case ip.IsLinkLocalMulticast(): + continue + case ip.IsInterfaceLocalMulticast(): + continue + case l.core.config.peerFilter != nil && !l.core.config.peerFilter(ip): + continue + } + ips = append(ips, ip) + } + if len(ips) == 0 { + return nil, ErrLinkNoSuitableIPs + } + for _, ip := range ips { + var conn net.Conn + if conn, err = fn(host, ip, port); err != nil { + url := *url + url.RawQuery = "" + l.core.log.Debugln("Dialling", url.Redacted(), "reported error:", err) + continue + } + return conn, nil } return nil, err } -func (intf *link) close() { - intf.conn.Close() -} - -func (intf *link) name() string { - return intf.lname +func urlForLinkInfo(u url.URL) url.URL { + u.RawQuery = "" + return u } type linkConn struct { // tx and rx are at the beginning of the struct to ensure 64-bit alignment // on 32-bit platforms, see https://pkg.go.dev/sync/atomic#pkg-note-BUG - rx uint64 - tx uint64 - up time.Time + rx uint64 + tx uint64 + rxrate uint64 + txrate uint64 + lastrx uint64 + lasttx uint64 + up time.Time net.Conn } diff --git a/src/core/link_quic.go b/src/core/link_quic.go new file mode 100644 index 00000000..d23ab184 --- /dev/null +++ b/src/core/link_quic.go @@ -0,0 +1,108 @@ +package core + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "net/url" + "time" + + "github.com/Arceliar/phony" + "github.com/quic-go/quic-go" +) + +type linkQUIC struct { + phony.Inbox + *links + tlsconfig *tls.Config + quicconfig *quic.Config +} + +type linkQUICStream struct { + quic.Connection + quic.Stream +} + +type linkQUICListener struct { + *quic.Listener + ch <-chan *linkQUICStream +} + +func (l *linkQUICListener) Accept() (net.Conn, error) { + qs := <-l.ch + if qs == nil { + return nil, context.Canceled + } + return qs, nil +} + +func (l *links) newLinkQUIC() *linkQUIC { + lt := &linkQUIC{ + links: l, + tlsconfig: l.core.config.tls.Clone(), + quicconfig: &quic.Config{ + MaxIdleTimeout: time.Minute, + KeepAlivePeriod: time.Second * 20, + TokenStore: quic.NewLRUTokenStore(255, 255), + }, + } + return lt +} + +func (l *linkQUIC) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) { + tlsconfig := l.tlsconfig.Clone() + return l.links.findSuitableIP(url, func(hostname string, ip net.IP, port int) (net.Conn, error) { + tlsconfig.ServerName = hostname + tlsconfig.MinVersion = tls.VersionTLS12 + tlsconfig.MaxVersion = tls.VersionTLS13 + hostport := net.JoinHostPort(ip.String(), fmt.Sprintf("%d", port)) + qc, err := quic.DialAddr(ctx, hostport, l.tlsconfig, l.quicconfig) + if err != nil { + return nil, err + } + qs, err := qc.OpenStreamSync(ctx) + if err != nil { + return nil, err + } + return &linkQUICStream{ + Connection: qc, + Stream: qs, + }, nil + }) +} + +func (l *linkQUIC) listen(ctx context.Context, url *url.URL, _ string) (net.Listener, error) { + ql, err := quic.ListenAddr(url.Host, l.tlsconfig, l.quicconfig) + if err != nil { + return nil, err + } + ch := make(chan *linkQUICStream) + lql := &linkQUICListener{ + Listener: ql, + ch: ch, + } + go func() { + for { + qc, err := ql.Accept(ctx) + switch err { + case context.Canceled, context.DeadlineExceeded: + ql.Close() + fallthrough + case quic.ErrServerClosed: + return + case nil: + qs, err := qc.AcceptStream(ctx) + if err != nil { + _ = qc.CloseWithError(1, fmt.Sprintf("stream error: %s", err)) + continue + } + ch <- &linkQUICStream{ + Connection: qc, + Stream: qs, + } + } + } + }() + return lql, nil +} diff --git a/src/core/link_socks.go b/src/core/link_socks.go new file mode 100644 index 00000000..495c8233 --- /dev/null +++ b/src/core/link_socks.go @@ -0,0 +1,67 @@ +package core + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "net/url" + "strings" + + "golang.org/x/net/proxy" +) + +type linkSOCKS struct { + *links +} + +func (l *links) newLinkSOCKS() *linkSOCKS { + lt := &linkSOCKS{ + links: l, + } + return lt +} + +func (l *linkSOCKS) dial(_ context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) { + var proxyAuth *proxy.Auth + if url.User != nil && url.User.Username() != "" { + proxyAuth = &proxy.Auth{ + User: url.User.Username(), + } + proxyAuth.Password, _ = url.User.Password() + } + tlsconfig := l.tls.config.Clone() + return l.links.findSuitableIP(url, func(hostname string, ip net.IP, port int) (net.Conn, error) { + hostport := net.JoinHostPort(ip.String(), fmt.Sprintf("%d", port)) + dialer, err := l.tcp.dialerFor(&net.TCPAddr{ + IP: ip, + Port: port, + }, info.sintf) + if err != nil { + return nil, err + } + proxy, err := proxy.SOCKS5("tcp", hostport, proxyAuth, dialer) + if err != nil { + return nil, err + } + pathtokens := strings.Split(strings.Trim(url.Path, "/"), "/") + conn, err := proxy.Dial("tcp", pathtokens[0]) + if err != nil { + return nil, err + } + if url.Scheme == "sockstls" { + tlsconfig.ServerName = hostname + tlsconfig.MinVersion = tls.VersionTLS12 + tlsconfig.MaxVersion = tls.VersionTLS13 + if sni := options.tlsSNI; sni != "" { + tlsconfig.ServerName = sni + } + conn = tls.Client(conn, tlsconfig) + } + return conn, nil + }) +} + +func (l *linkSOCKS) listen(ctx context.Context, url *url.URL, _ string) (net.Listener, error) { + return nil, fmt.Errorf("SOCKS listener not supported") +} diff --git a/src/core/link_tcp.go b/src/core/link_tcp.go new file mode 100644 index 00000000..e50912d3 --- /dev/null +++ b/src/core/link_tcp.go @@ -0,0 +1,111 @@ +package core + +import ( + "context" + "fmt" + "net" + "net/url" + "time" + + "github.com/Arceliar/phony" +) + +type linkTCP struct { + phony.Inbox + *links + listenconfig *net.ListenConfig +} + +func (l *links) newLinkTCP() *linkTCP { + lt := &linkTCP{ + links: l, + listenconfig: &net.ListenConfig{ + KeepAlive: -1, + }, + } + lt.listenconfig.Control = lt.tcpContext + return lt +} + +func (l *linkTCP) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) { + return l.links.findSuitableIP(url, func(hostname string, ip net.IP, port int) (net.Conn, error) { + addr := &net.TCPAddr{ + IP: ip, + Port: port, + } + dialer, err := l.tcp.dialerFor(addr, info.sintf) + if err != nil { + return nil, err + } + return dialer.DialContext(ctx, "tcp", addr.String()) + }) +} + +func (l *linkTCP) listen(ctx context.Context, url *url.URL, sintf string) (net.Listener, error) { + hostport := url.Host + if sintf != "" { + if host, port, err := net.SplitHostPort(hostport); err == nil { + hostport = fmt.Sprintf("[%s%%%s]:%s", host, sintf, port) + } + } + return l.listenconfig.Listen(ctx, "tcp", hostport) +} + +func (l *linkTCP) dialerFor(dst *net.TCPAddr, sintf string) (*net.Dialer, error) { + if dst.IP.IsLinkLocalUnicast() { + if sintf != "" { + dst.Zone = sintf + } + if dst.Zone == "" { + return nil, fmt.Errorf("link-local address requires a zone") + } + } + dialer := &net.Dialer{ + Timeout: time.Second * 5, + KeepAlive: -1, + Control: l.tcpContext, + } + if sintf != "" { + dialer.Control = l.getControl(sintf) + ief, err := net.InterfaceByName(sintf) + if err != nil { + return nil, fmt.Errorf("interface %q not found", sintf) + } + if ief.Flags&net.FlagUp == 0 { + return nil, fmt.Errorf("interface %q is not up", sintf) + } + addrs, err := ief.Addrs() + if err != nil { + return nil, fmt.Errorf("interface %q addresses not available: %w", sintf, err) + } + for addrindex, addr := range addrs { + src, _, err := net.ParseCIDR(addr.String()) + if err != nil { + continue + } + if !src.IsGlobalUnicast() && !src.IsLinkLocalUnicast() { + continue + } + bothglobal := src.IsGlobalUnicast() == dst.IP.IsGlobalUnicast() + bothlinklocal := src.IsLinkLocalUnicast() == dst.IP.IsLinkLocalUnicast() + if !bothglobal && !bothlinklocal { + continue + } + if (src.To4() != nil) != (dst.IP.To4() != nil) { + continue + } + if bothglobal || bothlinklocal || addrindex == len(addrs)-1 { + dialer.LocalAddr = &net.TCPAddr{ + IP: src, + Port: 0, + Zone: sintf, + } + break + } + } + if dialer.LocalAddr == nil { + return nil, fmt.Errorf("no suitable source address found on interface %q", sintf) + } + } + return dialer, nil +} diff --git a/src/core/tcp_darwin.go b/src/core/link_tcp_darwin.go similarity index 77% rename from src/core/tcp_darwin.go rename to src/core/link_tcp_darwin.go index 2ea3abc8..4c19ba93 100644 --- a/src/core/tcp_darwin.go +++ b/src/core/link_tcp_darwin.go @@ -11,7 +11,7 @@ import ( // WARNING: This context is used both by net.Dialer and net.Listen in tcp.go -func (t *tcp) tcpContext(network, address string, c syscall.RawConn) error { +func (t *linkTCP) tcpContext(network, address string, c syscall.RawConn) error { var control error var recvanyif error @@ -28,6 +28,6 @@ func (t *tcp) tcpContext(network, address string, c syscall.RawConn) error { } } -func (t *tcp) getControl(sintf string) func(string, string, syscall.RawConn) error { +func (t *linkTCP) getControl(_ string) func(string, string, syscall.RawConn) error { return t.tcpContext } diff --git a/src/core/link_tcp_linux.go b/src/core/link_tcp_linux.go new file mode 100644 index 00000000..6c54f30b --- /dev/null +++ b/src/core/link_tcp_linux.go @@ -0,0 +1,30 @@ +//go:build linux +// +build linux + +package core + +import ( + "syscall" + + "golang.org/x/sys/unix" +) + +// WARNING: This context is used both by net.Dialer and net.Listen in tcp.go + +func (t *linkTCP) tcpContext(network, address string, c syscall.RawConn) error { + return nil +} + +func (t *linkTCP) getControl(sintf string) func(string, string, syscall.RawConn) error { + return func(network, address string, c syscall.RawConn) error { + var err error + btd := func(fd uintptr) { + err = unix.BindToDevice(int(fd), sintf) + } + _ = c.Control(btd) + if err != nil { + t.links.core.log.Debugln("Failed to set SO_BINDTODEVICE:", sintf) + } + return t.tcpContext(network, address, c) + } +} diff --git a/src/core/tcp_other.go b/src/core/link_tcp_other.go similarity index 55% rename from src/core/tcp_other.go rename to src/core/link_tcp_other.go index 8dd76f28..f8a5aece 100644 --- a/src/core/tcp_other.go +++ b/src/core/link_tcp_other.go @@ -9,10 +9,10 @@ import ( // WARNING: This context is used both by net.Dialer and net.Listen in tcp.go -func (t *tcp) tcpContext(network, address string, c syscall.RawConn) error { +func (t *linkTCP) tcpContext(network, address string, c syscall.RawConn) error { return nil } -func (t *tcp) getControl(sintf string) func(string, string, syscall.RawConn) error { +func (t *linkTCP) getControl(sintf string) func(string, string, syscall.RawConn) error { return t.tcpContext } diff --git a/src/core/link_tls.go b/src/core/link_tls.go new file mode 100644 index 00000000..55da8597 --- /dev/null +++ b/src/core/link_tls.go @@ -0,0 +1,72 @@ +package core + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "net/url" + + "github.com/Arceliar/phony" +) + +type linkTLS struct { + phony.Inbox + *links + tcp *linkTCP + listener *net.ListenConfig + config *tls.Config +} + +func (l *links) newLinkTLS(tcp *linkTCP) *linkTLS { + lt := &linkTLS{ + links: l, + tcp: tcp, + listener: &net.ListenConfig{ + Control: tcp.tcpContext, + KeepAlive: -1, + }, + config: l.core.config.tls.Clone(), + } + return lt +} + +func (l *linkTLS) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) { + tlsconfig := l.config.Clone() + return l.links.findSuitableIP(url, func(hostname string, ip net.IP, port int) (net.Conn, error) { + tlsconfig.ServerName = hostname + tlsconfig.MinVersion = tls.VersionTLS12 + tlsconfig.MaxVersion = tls.VersionTLS13 + if sni := options.tlsSNI; sni != "" { + tlsconfig.ServerName = sni + } + addr := &net.TCPAddr{ + IP: ip, + Port: port, + } + dialer, err := l.tcp.dialerFor(addr, info.sintf) + if err != nil { + return nil, err + } + tlsdialer := &tls.Dialer{ + NetDialer: dialer, + Config: tlsconfig, + } + return tlsdialer.DialContext(ctx, "tcp", addr.String()) + }) +} + +func (l *linkTLS) listen(ctx context.Context, url *url.URL, sintf string) (net.Listener, error) { + hostport := url.Host + if sintf != "" { + if host, port, err := net.SplitHostPort(hostport); err == nil { + hostport = fmt.Sprintf("[%s%%%s]:%s", host, sintf, port) + } + } + listener, err := l.listener.Listen(ctx, "tcp", hostport) + if err != nil { + return nil, err + } + tlslistener := tls.NewListener(listener, l.config) + return tlslistener, nil +} diff --git a/src/core/link_unix.go b/src/core/link_unix.go new file mode 100644 index 00000000..ddbfa0ad --- /dev/null +++ b/src/core/link_unix.go @@ -0,0 +1,43 @@ +package core + +import ( + "context" + "net" + "net/url" + "time" + + "github.com/Arceliar/phony" +) + +type linkUNIX struct { + phony.Inbox + *links + dialer *net.Dialer + listener *net.ListenConfig +} + +func (l *links) newLinkUNIX() *linkUNIX { + lt := &linkUNIX{ + links: l, + dialer: &net.Dialer{ + Timeout: time.Second * 5, + KeepAlive: -1, + }, + listener: &net.ListenConfig{ + KeepAlive: -1, + }, + } + return lt +} + +func (l *linkUNIX) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) { + addr, err := net.ResolveUnixAddr("unix", url.Path) + if err != nil { + return nil, err + } + return l.dialer.DialContext(ctx, "unix", addr.String()) +} + +func (l *linkUNIX) listen(ctx context.Context, url *url.URL, _ string) (net.Listener, error) { + return l.listener.Listen(ctx, "unix", url.Path) +} diff --git a/src/core/link_ws.go b/src/core/link_ws.go new file mode 100644 index 00000000..86f065a6 --- /dev/null +++ b/src/core/link_ws.go @@ -0,0 +1,148 @@ +package core + +import ( + "context" + "fmt" + "net" + "net/http" + "net/url" + "time" + + "github.com/Arceliar/phony" + "github.com/coder/websocket" +) + +type linkWS struct { + phony.Inbox + *links + listenconfig *net.ListenConfig +} + +type linkWSConn struct { + net.Conn +} + +type linkWSListener struct { + ch chan *linkWSConn + ctx context.Context + httpServer *http.Server + listener net.Listener +} + +type wsServer struct { + ch chan *linkWSConn + ctx context.Context +} + +func (l *linkWSListener) Accept() (net.Conn, error) { + qs := <-l.ch + if qs == nil { + return nil, context.Canceled + } + return qs, nil +} + +func (l *linkWSListener) Addr() net.Addr { + return l.listener.Addr() +} + +func (l *linkWSListener) Close() error { + if err := l.httpServer.Shutdown(l.ctx); err != nil { + return err + } + return l.listener.Close() +} + +func (s *wsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/health" || r.URL.Path == "/healthz" { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("OK")) + return + } + + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + Subprotocols: []string{"ygg-ws"}, + }) + if err != nil { + return + } + + if c.Subprotocol() != "ygg-ws" { + c.Close(websocket.StatusPolicyViolation, "client must speak the ygg-ws subprotocol") + return + } + + s.ch <- &linkWSConn{ + Conn: websocket.NetConn(s.ctx, c, websocket.MessageBinary), + } +} + +func (l *links) newLinkWS() *linkWS { + lt := &linkWS{ + links: l, + listenconfig: &net.ListenConfig{ + KeepAlive: -1, + }, + } + return lt +} + +func (l *linkWS) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) { + return l.links.findSuitableIP(url, func(hostname string, ip net.IP, port int) (net.Conn, error) { + u := *url + u.Host = net.JoinHostPort(ip.String(), fmt.Sprintf("%d", port)) + addr := &net.TCPAddr{ + IP: ip, + Port: port, + } + dialer, err := l.tcp.dialerFor(addr, info.sintf) + if err != nil { + return nil, err + } + wsconn, _, err := websocket.Dial(ctx, u.String(), &websocket.DialOptions{ + HTTPClient: &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: dialer.Dial, + DialContext: dialer.DialContext, + }, + }, + Subprotocols: []string{"ygg-ws"}, + Host: hostname, + }) + if err != nil { + return nil, err + } + return &linkWSConn{ + Conn: websocket.NetConn(ctx, wsconn, websocket.MessageBinary), + }, nil + }) +} + +func (l *linkWS) listen(ctx context.Context, url *url.URL, _ string) (net.Listener, error) { + nl, err := l.listenconfig.Listen(ctx, "tcp", url.Host) + if err != nil { + return nil, err + } + + ch := make(chan *linkWSConn) + + httpServer := &http.Server{ + Handler: &wsServer{ + ch: ch, + ctx: ctx, + }, + BaseContext: func(_ net.Listener) context.Context { return ctx }, + ReadTimeout: time.Second * 10, + WriteTimeout: time.Second * 10, + } + + lwl := &linkWSListener{ + ch: ch, + ctx: ctx, + httpServer: httpServer, + listener: nl, + } + go lwl.httpServer.Serve(nl) // nolint:errcheck + return lwl, nil +} diff --git a/src/core/link_wss.go b/src/core/link_wss.go new file mode 100644 index 00000000..1d618324 --- /dev/null +++ b/src/core/link_wss.go @@ -0,0 +1,72 @@ +package core + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "net/http" + "net/url" + + "github.com/Arceliar/phony" + "github.com/coder/websocket" +) + +type linkWSS struct { + phony.Inbox + *links + tlsconfig *tls.Config +} + +type linkWSSConn struct { + net.Conn +} + +func (l *links) newLinkWSS() *linkWSS { + lwss := &linkWSS{ + links: l, + tlsconfig: l.core.config.tls.Clone(), + } + return lwss +} + +func (l *linkWSS) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) { + tlsconfig := l.tlsconfig.Clone() + return l.links.findSuitableIP(url, func(hostname string, ip net.IP, port int) (net.Conn, error) { + tlsconfig.ServerName = hostname + tlsconfig.MinVersion = tls.VersionTLS12 + tlsconfig.MaxVersion = tls.VersionTLS13 + u := *url + u.Host = net.JoinHostPort(ip.String(), fmt.Sprintf("%d", port)) + addr := &net.TCPAddr{ + IP: ip, + Port: port, + } + dialer, err := l.tcp.dialerFor(addr, info.sintf) + if err != nil { + return nil, err + } + wsconn, _, err := websocket.Dial(ctx, u.String(), &websocket.DialOptions{ + HTTPClient: &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: dialer.Dial, + DialContext: dialer.DialContext, + TLSClientConfig: tlsconfig, + }, + }, + Subprotocols: []string{"ygg-ws"}, + Host: hostname, + }) + if err != nil { + return nil, err + } + return &linkWSSConn{ + Conn: websocket.NetConn(ctx, wsconn, websocket.MessageBinary), + }, nil + }) +} + +func (l *linkWSS) listen(ctx context.Context, url *url.URL, _ string) (net.Listener, error) { + return nil, fmt.Errorf("WSS listener not supported, use WS listener behind reverse proxy instead") +} diff --git a/src/core/nodeinfo.go b/src/core/nodeinfo.go index 4ca21d73..6f1f4be8 100644 --- a/src/core/nodeinfo.go +++ b/src/core/nodeinfo.go @@ -4,31 +4,24 @@ import ( "encoding/hex" "encoding/json" "errors" - "net" + "fmt" "runtime" - "strings" "time" iwt "github.com/Arceliar/ironwood/types" "github.com/Arceliar/phony" - - //"github.com/yggdrasil-network/yggdrasil-go/src/crypto" - "github.com/yggdrasil-network/yggdrasil-go/src/address" "github.com/yggdrasil-network/yggdrasil-go/src/version" ) -// NodeInfoPayload represents a RequestNodeInfo response, in bytes. -type NodeInfoPayload []byte - type nodeinfo struct { phony.Inbox proto *protoHandler - myNodeInfo NodeInfoPayload + myNodeInfo json.RawMessage callbacks map[keyArray]nodeinfoCallback } type nodeinfoCallback struct { - call func(nodeinfo NodeInfoPayload) + call func(nodeinfo json.RawMessage) created time.Time } @@ -57,7 +50,7 @@ func (m *nodeinfo) _cleanup() { }) } -func (m *nodeinfo) _addCallback(sender keyArray, call func(nodeinfo NodeInfoPayload)) { +func (m *nodeinfo) _addCallback(sender keyArray, call func(nodeinfo json.RawMessage)) { m.callbacks[sender] = nodeinfoCallback{ created: time.Now(), call: call, @@ -65,67 +58,55 @@ func (m *nodeinfo) _addCallback(sender keyArray, call func(nodeinfo NodeInfoPayl } // Handles the callback, if there is one -func (m *nodeinfo) _callback(sender keyArray, nodeinfo NodeInfoPayload) { +func (m *nodeinfo) _callback(sender keyArray, nodeinfo json.RawMessage) { if callback, ok := m.callbacks[sender]; ok { callback.call(nodeinfo) delete(m.callbacks, sender) } } -func (m *nodeinfo) _getNodeInfo() NodeInfoPayload { +func (m *nodeinfo) _getNodeInfo() json.RawMessage { return m.myNodeInfo } // Set the current node's nodeinfo -func (m *nodeinfo) setNodeInfo(given interface{}, privacy bool) (err error) { +func (m *nodeinfo) setNodeInfo(given map[string]interface{}, privacy bool) (err error) { phony.Block(m, func() { err = m._setNodeInfo(given, privacy) }) return } -func (m *nodeinfo) _setNodeInfo(given interface{}, privacy bool) error { - defaults := map[string]interface{}{ - "buildname": version.BuildName(), - "buildversion": version.BuildVersion(), - "buildplatform": runtime.GOOS, - "buildarch": runtime.GOARCH, +func (m *nodeinfo) _setNodeInfo(given map[string]interface{}, privacy bool) error { + newnodeinfo := make(map[string]interface{}, len(given)) + for k, v := range given { + newnodeinfo[k] = v } - newnodeinfo := make(map[string]interface{}) if !privacy { - for k, v := range defaults { - newnodeinfo[k] = v - } - } - if nodeinfomap, ok := given.(map[string]interface{}); ok { - for key, value := range nodeinfomap { - if _, ok := defaults[key]; ok { - if strvalue, strok := value.(string); strok && strings.EqualFold(strvalue, "null") || value == nil { - delete(newnodeinfo, key) - } - continue - } - newnodeinfo[key] = value - } + newnodeinfo["buildname"] = version.BuildName() + newnodeinfo["buildversion"] = version.BuildVersion() + newnodeinfo["buildplatform"] = runtime.GOOS + newnodeinfo["buildarch"] = runtime.GOARCH } newjson, err := json.Marshal(newnodeinfo) - if err == nil { - if len(newjson) > 16384 { - return errors.New("NodeInfo exceeds max length of 16384 bytes") - } + switch { + case err != nil: + return fmt.Errorf("NodeInfo marshalling failed: %w", err) + case len(newjson) > 16384: + return fmt.Errorf("NodeInfo exceeds max length of 16384 bytes") + default: m.myNodeInfo = newjson return nil } - return err } -func (m *nodeinfo) sendReq(from phony.Actor, key keyArray, callback func(nodeinfo NodeInfoPayload)) { +func (m *nodeinfo) sendReq(from phony.Actor, key keyArray, callback func(nodeinfo json.RawMessage)) { m.Act(from, func() { m._sendReq(key, callback) }) } -func (m *nodeinfo) _sendReq(key keyArray, callback func(nodeinfo NodeInfoPayload)) { +func (m *nodeinfo) _sendReq(key keyArray, callback func(nodeinfo json.RawMessage)) { if callback != nil { m._addCallback(key, callback) } @@ -138,7 +119,7 @@ func (m *nodeinfo) handleReq(from phony.Actor, key keyArray) { }) } -func (m *nodeinfo) handleRes(from phony.Actor, key keyArray, info NodeInfoPayload) { +func (m *nodeinfo) handleRes(from phony.Actor, key keyArray, info json.RawMessage) { m.Act(from, func() { m._callback(key, info) }) @@ -154,36 +135,39 @@ func (m *nodeinfo) _sendRes(key keyArray) { type GetNodeInfoRequest struct { Key string `json:"key"` } -type GetNodeInfoResponse map[string]interface{} +type GetNodeInfoResponse map[string]json.RawMessage func (m *nodeinfo) nodeInfoAdminHandler(in json.RawMessage) (interface{}, error) { var req GetNodeInfoRequest if err := json.Unmarshal(in, &req); err != nil { return nil, err } + if req.Key == "" { + return nil, fmt.Errorf("No remote public key supplied") + } var key keyArray var kbs []byte var err error if kbs, err = hex.DecodeString(req.Key); err != nil { - return nil, err + return nil, fmt.Errorf("Failed to decode public key: %w", err) } copy(key[:], kbs) ch := make(chan []byte, 1) - m.sendReq(nil, key, func(info NodeInfoPayload) { + m.sendReq(nil, key, func(info json.RawMessage) { ch <- info }) timer := time.NewTimer(6 * time.Second) defer timer.Stop() select { case <-timer.C: - return nil, errors.New("timeout") + return nil, errors.New("Timed out waiting for response") case info := <-ch: var msg json.RawMessage if err := msg.UnmarshalJSON(info); err != nil { return nil, err } - ip := net.IP(address.AddrForKey(kbs)[:]) - res := GetNodeInfoResponse{ip.String(): msg} + key := hex.EncodeToString(kbs[:]) + res := GetNodeInfoResponse{key: msg} return res, nil } } diff --git a/src/core/options.go b/src/core/options.go new file mode 100644 index 00000000..7e67bfb4 --- /dev/null +++ b/src/core/options.go @@ -0,0 +1,61 @@ +package core + +import ( + "crypto/ed25519" + "fmt" + "net" + "net/url" +) + +func (c *Core) _applyOption(opt SetupOption) (err error) { + switch v := opt.(type) { + case Peer: + u, err := url.Parse(v.URI) + if err != nil { + return fmt.Errorf("unable to parse peering URI: %w", err) + } + err = c.links.add(u, v.SourceInterface, linkTypePersistent) + switch err { + case ErrLinkAlreadyConfigured: + // Don't return this error, otherwise we'll panic at startup + // if there are multiple of the same peer configured + return nil + default: + return err + } + case ListenAddress: + c.config._listeners[v] = struct{}{} + case PeerFilter: + c.config.peerFilter = v + case NodeInfo: + c.config.nodeinfo = v + case NodeInfoPrivacy: + c.config.nodeinfoPrivacy = v + case AllowedPublicKey: + pk := [32]byte{} + copy(pk[:], v) + c.config._allowedPublicKeys[pk] = struct{}{} + } + return +} + +type SetupOption interface { + isSetupOption() +} + +type ListenAddress string +type Peer struct { + URI string + SourceInterface string +} +type NodeInfo map[string]interface{} +type NodeInfoPrivacy bool +type AllowedPublicKey ed25519.PublicKey +type PeerFilter func(net.IP) bool + +func (a ListenAddress) isSetupOption() {} +func (a Peer) isSetupOption() {} +func (a NodeInfo) isSetupOption() {} +func (a NodeInfoPrivacy) isSetupOption() {} +func (a AllowedPublicKey) isSetupOption() {} +func (a PeerFilter) isSetupOption() {} diff --git a/src/core/options_test.go b/src/core/options_test.go new file mode 100644 index 00000000..bab22fb1 --- /dev/null +++ b/src/core/options_test.go @@ -0,0 +1,41 @@ +package core + +import ( + "net/url" + "testing" + + "github.com/yggdrasil-network/yggdrasil-go/src/config" +) + +// Tests that duplicate peers in the configuration file +// won't cause an error when the node starts. Otherwise +// we can panic unnecessarily. +func TestDuplicatePeerAtStartup(t *testing.T) { + cfg := config.GenerateConfig() + for i := 0; i < 5; i++ { + cfg.Peers = append(cfg.Peers, "tcp://1.2.3.4:4321") + } + if _, err := New(cfg.Certificate, nil); err != nil { + t.Fatal(err) + } +} + +// Tests that duplicate peers given to us through the +// API will still error as expected, even if they didn't +// at startup. We expect to notify the user through the +// admin socket if they try to add a peer that is already +// configured. +func TestDuplicatePeerFromAPI(t *testing.T) { + cfg := config.GenerateConfig() + c, err := New(cfg.Certificate, nil) + if err != nil { + t.Fatal(err) + } + u, _ := url.Parse("tcp://1.2.3.4:4321") + if err := c.AddPeer(u, ""); err != nil { + t.Fatalf("Adding peer failed on first attempt: %s", err) + } + if err := c.AddPeer(u, ""); err == nil { + t.Fatalf("Adding peer should have failed on second attempt") + } +} diff --git a/src/core/pool.go b/src/core/pool.go new file mode 100644 index 00000000..7b1e93ed --- /dev/null +++ b/src/core/pool.go @@ -0,0 +1,17 @@ +package core + +import "sync" + +var bytePool = sync.Pool{New: func() interface{} { return []byte(nil) }} + +func allocBytes(size int) []byte { + bs := bytePool.Get().([]byte) + if cap(bs) < size { + bs = make([]byte, size) + } + return bs[:size] +} + +func freeBytes(bs []byte) { + bytePool.Put(bs[:0]) //nolint:staticcheck +} diff --git a/src/core/proto.go b/src/core/proto.go index 3c68c0e3..bffc2022 100644 --- a/src/core/proto.go +++ b/src/core/proto.go @@ -21,8 +21,8 @@ const ( typeDebugGetSelfResponse typeDebugGetPeersRequest typeDebugGetPeersResponse - typeDebugGetDHTRequest - typeDebugGetDHTResponse + typeDebugGetTreeRequest + typeDebugGetTreeResponse ) type reqInfo struct { @@ -40,7 +40,7 @@ type protoHandler struct { selfRequests map[keyArray]*reqInfo peersRequests map[keyArray]*reqInfo - dhtRequests map[keyArray]*reqInfo + treeRequests map[keyArray]*reqInfo } func (p *protoHandler) init(core *Core) { @@ -49,7 +49,7 @@ func (p *protoHandler) init(core *Core) { p.selfRequests = make(map[keyArray]*reqInfo) p.peersRequests = make(map[keyArray]*reqInfo) - p.dhtRequests = make(map[keyArray]*reqInfo) + p.treeRequests = make(map[keyArray]*reqInfo) } // Common functions @@ -89,10 +89,10 @@ func (p *protoHandler) _handleDebug(key keyArray, bs []byte) { p._handleGetPeersRequest(key) case typeDebugGetPeersResponse: p._handleGetPeersResponse(key, bs[1:]) - case typeDebugGetDHTRequest: - p._handleGetDHTRequest(key) - case typeDebugGetDHTResponse: - p._handleGetDHTResponse(key, bs[1:]) + case typeDebugGetTreeRequest: + p._handleGetTreeRequest(key) + case typeDebugGetTreeResponse: + p._handleGetTreeResponse(key, bs[1:]) } } @@ -126,8 +126,8 @@ func (p *protoHandler) sendGetSelfRequest(key keyArray, callback func([]byte)) { func (p *protoHandler) _handleGetSelfRequest(key keyArray) { self := p.core.GetSelf() res := map[string]string{ - "key": hex.EncodeToString(self.Key[:]), - "coords": fmt.Sprintf("%v", self.Coords), + "key": hex.EncodeToString(self.Key[:]), + "routing_entries": fmt.Sprintf("%v", self.RoutingEntries), } bs, err := json.Marshal(res) // FIXME this puts keys in base64, not hex if err != nil { @@ -188,47 +188,47 @@ func (p *protoHandler) _handleGetPeersResponse(key keyArray, bs []byte) { } } -// Get DHT +// Get Tree -func (p *protoHandler) sendGetDHTRequest(key keyArray, callback func([]byte)) { +func (p *protoHandler) sendGetTreeRequest(key keyArray, callback func([]byte)) { p.Act(nil, func() { - if info := p.dhtRequests[key]; info != nil { + if info := p.treeRequests[key]; info != nil { info.timer.Stop() - delete(p.dhtRequests, key) + delete(p.treeRequests, key) } info := new(reqInfo) info.callback = callback info.timer = time.AfterFunc(time.Minute, func() { p.Act(nil, func() { - if p.dhtRequests[key] == info { - delete(p.dhtRequests, key) + if p.treeRequests[key] == info { + delete(p.treeRequests, key) } }) }) - p.dhtRequests[key] = info - p._sendDebug(key, typeDebugGetDHTRequest, nil) + p.treeRequests[key] = info + p._sendDebug(key, typeDebugGetTreeRequest, nil) }) } -func (p *protoHandler) _handleGetDHTRequest(key keyArray) { - dinfos := p.core.GetDHT() +func (p *protoHandler) _handleGetTreeRequest(key keyArray) { + dinfos := p.core.GetTree() var bs []byte for _, dinfo := range dinfos { tmp := append(bs, dinfo.Key[:]...) - const responseOverhead = 2 // 1 debug type, 1 getdht type + const responseOverhead = 2 // 1 debug type, 1 gettree type if uint64(len(tmp))+responseOverhead > p.core.MTU() { break } bs = tmp } - p._sendDebug(key, typeDebugGetDHTResponse, bs) + p._sendDebug(key, typeDebugGetTreeResponse, bs) } -func (p *protoHandler) _handleGetDHTResponse(key keyArray, bs []byte) { - if info := p.dhtRequests[key]; info != nil { +func (p *protoHandler) _handleGetTreeResponse(key keyArray, bs []byte) { + if info := p.treeRequests[key]; info != nil { info.timer.Stop() info.callback(bs) - delete(p.dhtRequests, key) + delete(p.treeRequests, key) } } @@ -251,15 +251,16 @@ func (p *protoHandler) getSelfHandler(in json.RawMessage) (interface{}, error) { if kbs, err = hex.DecodeString(req.Key); err != nil { return nil, err } + if len(kbs) != ed25519.PublicKeySize { + return nil, fmt.Errorf("invalid public key length") + } copy(key[:], kbs) ch := make(chan []byte, 1) p.sendGetSelfRequest(key, func(info []byte) { ch <- info }) - timer := time.NewTimer(6 * time.Second) - defer timer.Stop() select { - case <-timer.C: + case <-time.After(6 * time.Second): return nil, errors.New("timeout") case info := <-ch: var msg json.RawMessage @@ -291,15 +292,16 @@ func (p *protoHandler) getPeersHandler(in json.RawMessage) (interface{}, error) if kbs, err = hex.DecodeString(req.Key); err != nil { return nil, err } + if len(kbs) != ed25519.PublicKeySize { + return nil, fmt.Errorf("invalid public key length") + } copy(key[:], kbs) ch := make(chan []byte, 1) p.sendGetPeersRequest(key, func(info []byte) { ch <- info }) - timer := time.NewTimer(6 * time.Second) - defer timer.Stop() select { - case <-timer.C: + case <-time.After(6 * time.Second): return nil, errors.New("timeout") case info := <-ch: ks := make(map[string][]string) @@ -322,16 +324,16 @@ func (p *protoHandler) getPeersHandler(in json.RawMessage) (interface{}, error) } } -// Admin socket stuff for "Get DHT" +// Admin socket stuff for "Get Tree" -type DebugGetDHTRequest struct { +type DebugGetTreeRequest struct { Key string `json:"key"` } -type DebugGetDHTResponse map[string]interface{} +type DebugGetTreeResponse map[string]interface{} -func (p *protoHandler) getDHTHandler(in json.RawMessage) (interface{}, error) { - var req DebugGetDHTRequest +func (p *protoHandler) getTreeHandler(in json.RawMessage) (interface{}, error) { + var req DebugGetTreeRequest if err := json.Unmarshal(in, &req); err != nil { return nil, err } @@ -341,15 +343,16 @@ func (p *protoHandler) getDHTHandler(in json.RawMessage) (interface{}, error) { if kbs, err = hex.DecodeString(req.Key); err != nil { return nil, err } + if len(kbs) != ed25519.PublicKeySize { + return nil, fmt.Errorf("invalid public key length") + } copy(key[:], kbs) ch := make(chan []byte, 1) - p.sendGetDHTRequest(key, func(info []byte) { + p.sendGetTreeRequest(key, func(info []byte) { ch <- info }) - timer := time.NewTimer(6 * time.Second) - defer timer.Stop() select { - case <-timer.C: + case <-time.After(6 * time.Second): return nil, errors.New("timeout") case info := <-ch: ks := make(map[string][]string) @@ -367,7 +370,7 @@ func (p *protoHandler) getDHTHandler(in json.RawMessage) (interface{}, error) { return nil, err } ip := net.IP(address.AddrForKey(kbs)[:]) - res := DebugGetDHTResponse{ip.String(): msg} + res := DebugGetTreeResponse{ip.String(): msg} return res, nil } } diff --git a/src/core/tcp.go b/src/core/tcp.go deleted file mode 100644 index 7b1773b8..00000000 --- a/src/core/tcp.go +++ /dev/null @@ -1,418 +0,0 @@ -package core - -// This sends packets to peers using TCP as a transport -// It's generally better tested than the UDP implementation -// Using it regularly is insane, but I find TCP easier to test/debug with it -// Updating and optimizing the UDP version is a higher priority - -// TODO: -// Something needs to make sure we're getting *valid* packets -// Could be used to DoS (connect, give someone else's keys, spew garbage) -// I guess the "peer" part should watch for link packets, disconnect? - -// TCP connections start with a metadata exchange. -// It involves exchanging version numbers and crypto keys -// See version.go for version metadata format - -import ( - "context" - "fmt" - "math/rand" - "net" - "net/url" - "strings" - "sync" - "time" - - "golang.org/x/net/proxy" - - "github.com/yggdrasil-network/yggdrasil-go/src/address" - //"github.com/yggdrasil-network/yggdrasil-go/src/util" -) - -const default_timeout = 6 * time.Second - -// The TCP listener and information about active TCP connections, to avoid duplication. -type tcp struct { - links *links - waitgroup sync.WaitGroup - mutex sync.Mutex // Protecting the below - listeners map[string]*TcpListener - calls map[string]struct{} - conns map[linkInfo](chan struct{}) - tls tcptls -} - -// TcpListener is a stoppable TCP listener interface. These are typically -// returned from calls to the ListenTCP() function and are also used internally -// to represent listeners created by the "Listen" configuration option and for -// multicast interfaces. -type TcpListener struct { - Listener net.Listener - opts tcpOptions - stop chan struct{} -} - -type TcpUpgrade struct { - upgrade func(c net.Conn, o *tcpOptions) (net.Conn, error) - name string -} - -type tcpOptions struct { - linkOptions - upgrade *TcpUpgrade - socksProxyAddr string - socksProxyAuth *proxy.Auth - socksPeerAddr string - tlsSNI string -} - -func (l *TcpListener) Stop() { - defer func() { _ = recover() }() - close(l.stop) -} - -// Wrapper function to set additional options for specific connection types. -func (t *tcp) setExtraOptions(c net.Conn) { - switch sock := c.(type) { - case *net.TCPConn: - _ = sock.SetNoDelay(true) - // TODO something for socks5 - default: - } -} - -// Returns the address of the listener. -func (t *tcp) getAddr() *net.TCPAddr { - // TODO: Fix this, because this will currently only give a single address - // to multicast.go, which obviously is not great, but right now multicast.go - // doesn't have the ability to send more than one address in a packet either - t.mutex.Lock() - defer t.mutex.Unlock() - for _, l := range t.listeners { - return l.Listener.Addr().(*net.TCPAddr) - } - return nil -} - -// Initializes the struct. -func (t *tcp) init(l *links) error { - t.links = l - t.tls.init(t) - t.mutex.Lock() - t.calls = make(map[string]struct{}) - t.conns = make(map[linkInfo](chan struct{})) - t.listeners = make(map[string]*TcpListener) - t.mutex.Unlock() - - t.links.core.config.RLock() - defer t.links.core.config.RUnlock() - for _, listenaddr := range t.links.core.config.Listen { - u, err := url.Parse(listenaddr) - if err != nil { - t.links.core.log.Errorln("Failed to parse listener: listener", listenaddr, "is not correctly formatted, ignoring") - } - if _, err := t.listenURL(u, ""); err != nil { - return err - } - } - - return nil -} - -func (t *tcp) stop() error { - t.mutex.Lock() - for _, listener := range t.listeners { - listener.Stop() - } - t.mutex.Unlock() - t.waitgroup.Wait() - return nil -} - -func (t *tcp) listenURL(u *url.URL, sintf string) (*TcpListener, error) { - var listener *TcpListener - var err error - hostport := u.Host // Used for tcp and tls - if len(sintf) != 0 { - host, port, err := net.SplitHostPort(hostport) - if err == nil { - hostport = fmt.Sprintf("[%s%%%s]:%s", host, sintf, port) - } - } - switch u.Scheme { - case "tcp": - listener, err = t.listen(hostport, nil) - case "tls": - listener, err = t.listen(hostport, t.tls.forListener) - default: - t.links.core.log.Errorln("Failed to add listener: listener", u.String(), "is not correctly formatted, ignoring") - } - return listener, err -} - -func (t *tcp) listen(listenaddr string, upgrade *TcpUpgrade) (*TcpListener, error) { - var err error - - ctx := t.links.core.ctx - lc := net.ListenConfig{ - Control: t.tcpContext, - } - listener, err := lc.Listen(ctx, "tcp", listenaddr) - if err == nil { - l := TcpListener{ - Listener: listener, - opts: tcpOptions{upgrade: upgrade}, - stop: make(chan struct{}), - } - t.waitgroup.Add(1) - go t.listener(&l, listenaddr) - return &l, nil - } - - return nil, err -} - -// Runs the listener, which spawns off goroutines for incoming connections. -func (t *tcp) listener(l *TcpListener, listenaddr string) { - defer t.waitgroup.Done() - if l == nil { - return - } - // Track the listener so that we can find it again in future - t.mutex.Lock() - if _, isIn := t.listeners[listenaddr]; isIn { - t.mutex.Unlock() - l.Listener.Close() - return - } - callproto := "TCP" - if l.opts.upgrade != nil { - callproto = strings.ToUpper(l.opts.upgrade.name) - } - t.listeners[listenaddr] = l - t.mutex.Unlock() - // And here we go! - defer func() { - t.links.core.log.Infoln("Stopping", callproto, "listener on:", l.Listener.Addr().String()) - l.Listener.Close() - t.mutex.Lock() - delete(t.listeners, listenaddr) - t.mutex.Unlock() - }() - t.links.core.log.Infoln("Listening for", callproto, "on:", l.Listener.Addr().String()) - go func() { - <-l.stop - l.Listener.Close() - }() - defer l.Stop() - for { - sock, err := l.Listener.Accept() - if err != nil { - t.links.core.log.Errorln("Failed to accept connection:", err) - select { - case <-l.stop: - return - default: - } - time.Sleep(time.Second) // So we don't busy loop - continue - } - t.waitgroup.Add(1) - options := l.opts - go t.handler(sock, true, options) - } -} - -// Checks if we already are calling this address -func (t *tcp) startCalling(saddr string) bool { - t.mutex.Lock() - defer t.mutex.Unlock() - _, isIn := t.calls[saddr] - t.calls[saddr] = struct{}{} - return !isIn -} - -// Checks if a connection already exists. -// If not, it adds it to the list of active outgoing calls (to block future attempts) and dials the address. -// If the dial is successful, it launches the handler. -// When finished, it removes the outgoing call, so reconnection attempts can be made later. -// This all happens in a separate goroutine that it spawns. -func (t *tcp) call(saddr string, options tcpOptions, sintf string) { - go func() { - callname := saddr - callproto := "TCP" - if options.upgrade != nil { - callproto = strings.ToUpper(options.upgrade.name) - } - if sintf != "" { - callname = fmt.Sprintf("%s/%s/%s", callproto, saddr, sintf) - } - if !t.startCalling(callname) { - return - } - defer func() { - // Block new calls for a little while, to mitigate livelock scenarios - rand.Seed(time.Now().UnixNano()) - delay := default_timeout + time.Duration(rand.Intn(10000))*time.Millisecond - time.Sleep(delay) - t.mutex.Lock() - delete(t.calls, callname) - t.mutex.Unlock() - }() - var conn net.Conn - var err error - if options.socksProxyAddr != "" { - if sintf != "" { - return - } - dialerdst, er := net.ResolveTCPAddr("tcp", options.socksProxyAddr) - if er != nil { - return - } - var dialer proxy.Dialer - dialer, err = proxy.SOCKS5("tcp", dialerdst.String(), options.socksProxyAuth, proxy.Direct) - if err != nil { - return - } - ctx, done := context.WithTimeout(t.links.core.ctx, default_timeout) - conn, err = dialer.(proxy.ContextDialer).DialContext(ctx, "tcp", saddr) - done() - if err != nil { - return - } - t.waitgroup.Add(1) - options.socksPeerAddr = saddr - if ch := t.handler(conn, false, options); ch != nil { - <-ch - } - } else { - dst, err := net.ResolveTCPAddr("tcp", saddr) - if err != nil { - return - } - if dst.IP.IsLinkLocalUnicast() { - dst.Zone = sintf - if dst.Zone == "" { - return - } - } - dialer := net.Dialer{ - Control: t.tcpContext, - } - if sintf != "" { - dialer.Control = t.getControl(sintf) - ief, err := net.InterfaceByName(sintf) - if err != nil { - return - } - if ief.Flags&net.FlagUp == 0 { - return - } - addrs, err := ief.Addrs() - if err == nil { - for addrindex, addr := range addrs { - src, _, err := net.ParseCIDR(addr.String()) - if err != nil { - continue - } - if src.Equal(dst.IP) { - continue - } - if !src.IsGlobalUnicast() && !src.IsLinkLocalUnicast() { - continue - } - bothglobal := src.IsGlobalUnicast() == dst.IP.IsGlobalUnicast() - bothlinklocal := src.IsLinkLocalUnicast() == dst.IP.IsLinkLocalUnicast() - if !bothglobal && !bothlinklocal { - continue - } - if (src.To4() != nil) != (dst.IP.To4() != nil) { - continue - } - if bothglobal || bothlinklocal || addrindex == len(addrs)-1 { - dialer.LocalAddr = &net.TCPAddr{ - IP: src, - Port: 0, - Zone: sintf, - } - break - } - } - if dialer.LocalAddr == nil { - return - } - } - } - ctx, done := context.WithTimeout(t.links.core.ctx, default_timeout) - conn, err = dialer.DialContext(ctx, "tcp", dst.String()) - done() - if err != nil { - t.links.core.log.Debugf("Failed to dial %s: %s", callproto, err) - return - } - t.waitgroup.Add(1) - if ch := t.handler(conn, false, options); ch != nil { - <-ch - } - } - }() -} - -func (t *tcp) handler(sock net.Conn, incoming bool, options tcpOptions) chan struct{} { - defer t.waitgroup.Done() // Happens after sock.close - defer sock.Close() - t.setExtraOptions(sock) - var upgraded bool - if options.upgrade != nil { - var err error - if sock, err = options.upgrade.upgrade(sock, &options); err != nil { - t.links.core.log.Errorln("TCP handler upgrade failed:", err) - return nil - } - upgraded = true - } - var name, proto, local, remote string - if options.socksProxyAddr != "" { - name = "socks://" + sock.RemoteAddr().String() + "/" + options.socksPeerAddr - proto = "socks" - local, _, _ = net.SplitHostPort(sock.LocalAddr().String()) - remote, _, _ = net.SplitHostPort(options.socksPeerAddr) - } else { - if upgraded { - proto = options.upgrade.name - name = proto + "://" + sock.RemoteAddr().String() - } else { - proto = "tcp" - name = proto + "://" + sock.RemoteAddr().String() - } - local, _, _ = net.SplitHostPort(sock.LocalAddr().String()) - remote, _, _ = net.SplitHostPort(sock.RemoteAddr().String()) - } - localIP := net.ParseIP(local) - if localIP = localIP.To16(); localIP != nil { - var laddr address.Address - var lsubnet address.Subnet - copy(laddr[:], localIP) - copy(lsubnet[:], localIP) - if laddr.IsValid() || lsubnet.IsValid() { - // The local address is with the network address/prefix range - // This would route ygg over ygg, which we don't want - // FIXME ideally this check should happen outside of the core library - // Maybe dial/listen at the application level - // Then pass a net.Conn to the core library (after these kinds of checks are done) - t.links.core.log.Debugln("Dropping ygg-tunneled connection", local, remote) - return nil - } - } - force := net.ParseIP(strings.Split(remote, "%")[0]).IsLinkLocalUnicast() - link, err := t.links.create(sock, name, proto, local, remote, incoming, force, options.linkOptions) - if err != nil { - t.links.core.log.Println(err) - panic(err) - } - t.links.core.log.Debugln("DEBUG: starting handler for", name) - ch, err := link.handler() - t.links.core.log.Debugln("DEBUG: stopped handler for", name, err) - return ch -} diff --git a/src/core/tcp_linux.go b/src/core/tcp_linux.go deleted file mode 100644 index e59c3121..00000000 --- a/src/core/tcp_linux.go +++ /dev/null @@ -1,46 +0,0 @@ -//go:build linux -// +build linux - -package core - -import ( - "syscall" - - "golang.org/x/sys/unix" -) - -// WARNING: This context is used both by net.Dialer and net.Listen in tcp.go - -func (t *tcp) tcpContext(network, address string, c syscall.RawConn) error { - var control error - var bbr error - - control = c.Control(func(fd uintptr) { - bbr = unix.SetsockoptString(int(fd), unix.IPPROTO_TCP, unix.TCP_CONGESTION, "bbr") - }) - - // Log any errors - if bbr != nil { - t.links.core.log.Debugln("Failed to set tcp_congestion_control to bbr for socket, SetsockoptString error:", bbr) - } - if control != nil { - t.links.core.log.Debugln("Failed to set tcp_congestion_control to bbr for socket, Control error:", control) - } - - // Return nil because errors here are not considered fatal for the connection, it just means congestion control is suboptimal - return nil -} - -func (t *tcp) getControl(sintf string) func(string, string, syscall.RawConn) error { - return func(network, address string, c syscall.RawConn) error { - var err error - btd := func(fd uintptr) { - err = unix.BindToDevice(int(fd), sintf) - } - _ = c.Control(btd) - if err != nil { - t.links.core.log.Debugln("Failed to set SO_BINDTODEVICE:", sintf) - } - return t.tcpContext(network, address, c) - } -} diff --git a/src/core/tls.go b/src/core/tls.go index 9e340ac4..0ac7e87a 100644 --- a/src/core/tls.go +++ b/src/core/tls.go @@ -1,126 +1,29 @@ package core import ( - "bytes" - "crypto/ed25519" - "crypto/rand" "crypto/tls" "crypto/x509" - "crypto/x509/pkix" - "encoding/hex" - "encoding/pem" - "errors" - "log" - "math/big" - "net" - "time" ) -type tcptls struct { - tcp *tcp - config *tls.Config - forDialer *TcpUpgrade - forListener *TcpUpgrade -} - -func (t *tcptls) init(tcp *tcp) { - t.tcp = tcp - t.forDialer = &TcpUpgrade{ - upgrade: t.upgradeDialer, - name: "tls", - } - t.forListener = &TcpUpgrade{ - upgrade: t.upgradeListener, - name: "tls", - } - - edpriv := make(ed25519.PrivateKey, ed25519.PrivateKeySize) - copy(edpriv[:], tcp.links.core.secret[:]) - - certBuf := &bytes.Buffer{} - - // TODO: because NotAfter is finite, we should add some mechanism to regenerate the certificate and restart the listeners periodically for nodes with very high uptimes. Perhaps regenerate certs and restart listeners every few months or so. - pubtemp := x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{ - CommonName: hex.EncodeToString(tcp.links.core.public[:]), +func (c *Core) generateTLSConfig(cert *tls.Certificate) (*tls.Config, error) { + config := &tls.Config{ + Certificates: []tls.Certificate{*cert}, + ClientAuth: tls.NoClientCert, + GetClientCertificate: func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) { + return cert, nil }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(time.Hour * 24 * 365), - KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - BasicConstraintsValid: true, - } - - derbytes, err := x509.CreateCertificate(rand.Reader, &pubtemp, &pubtemp, edpriv.Public(), edpriv) - if err != nil { - log.Fatalf("Failed to create certificate: %s", err) - } - - if err := pem.Encode(certBuf, &pem.Block{Type: "CERTIFICATE", Bytes: derbytes}); err != nil { - panic("failed to encode certificate into PEM") - } - - cpool := x509.NewCertPool() - cpool.AppendCertsFromPEM(derbytes) - - t.config = &tls.Config{ - RootCAs: cpool, - Certificates: []tls.Certificate{ - { - Certificate: [][]byte{derbytes}, - PrivateKey: edpriv, - }, - }, - InsecureSkipVerify: true, - MinVersion: tls.VersionTLS13, + VerifyPeerCertificate: c.verifyTLSCertificate, + VerifyConnection: c.verifyTLSConnection, + InsecureSkipVerify: true, + MinVersion: tls.VersionTLS13, } + return config, nil } -func (t *tcptls) configForOptions(options *tcpOptions) *tls.Config { - config := t.config.Clone() - config.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error { - if len(rawCerts) != 1 { - return errors.New("tls not exactly 1 cert") - } - cert, err := x509.ParseCertificate(rawCerts[0]) - if err != nil { - return errors.New("tls failed to parse cert") - } - if cert.PublicKeyAlgorithm != x509.Ed25519 { - return errors.New("tls wrong cert algorithm") - } - pk := cert.PublicKey.(ed25519.PublicKey) - var key keyArray - copy(key[:], pk) - // If options does not have a pinned key, then pin one now - if options.pinnedEd25519Keys == nil { - options.pinnedEd25519Keys = make(map[keyArray]struct{}) - options.pinnedEd25519Keys[key] = struct{}{} - } - if _, isIn := options.pinnedEd25519Keys[key]; !isIn { - return errors.New("tls key does not match pinned key") - } - return nil - } - return config +func (c *Core) verifyTLSCertificate(_ [][]byte, _ [][]*x509.Certificate) error { + return nil } -func (t *tcptls) upgradeListener(c net.Conn, options *tcpOptions) (net.Conn, error) { - config := t.configForOptions(options) - conn := tls.Server(c, config) - if err := conn.Handshake(); err != nil { - return c, err - } - return conn, nil -} - -func (t *tcptls) upgradeDialer(c net.Conn, options *tcpOptions) (net.Conn, error) { - config := t.configForOptions(options) - config.ServerName = options.tlsSNI - conn := tls.Client(c, config) - if err := conn.Handshake(); err != nil { - return c, err - } - return conn, nil +func (c *Core) verifyTLSConnection(_ tls.ConnectionState) error { + return nil } diff --git a/src/core/version.go b/src/core/version.go index 0bfbbcbc..bb3b9538 100644 --- a/src/core/version.go +++ b/src/core/version.go @@ -4,65 +4,166 @@ package core // Used in the initial connection setup and key exchange // Some of this could arguably go in wire.go instead -import "crypto/ed25519" +import ( + "bytes" + "crypto/ed25519" + "encoding/binary" + "io" + + "golang.org/x/crypto/blake2b" +) // This is the version-specific metadata exchanged at the start of a connection. // It must always begin with the 4 bytes "meta" and a wire formatted uint64 major version number. // The current version also includes a minor version number, and the box/sig/link keys that need to be exchanged to open a connection. type version_metadata struct { - meta [4]byte - ver uint8 // 1 byte in this version - // Everything after this point potentially depends on the version number, and is subject to change in future versions - minorVer uint8 // 1 byte in this version - key ed25519.PublicKey + majorVer uint16 + minorVer uint16 + publicKey ed25519.PublicKey + priority uint8 } +const ( + ProtocolVersionMajor uint16 = 0 + ProtocolVersionMinor uint16 = 5 +) + +// Once a major/minor version is released, it is not safe to change any of these +// (including their ordering), it is only safe to add new ones. +const ( + metaVersionMajor uint16 = iota // uint16 + metaVersionMinor // uint16 + metaPublicKey // [32]byte + metaPriority // uint8 +) + +type handshakeError string + +func (e handshakeError) Error() string { return string(e) } + +const ErrHandshakeInvalidPreamble = handshakeError("invalid handshake, remote side is not Yggdrasil") +const ErrHandshakeInvalidLength = handshakeError("invalid handshake length, possible version mismatch") +const ErrHandshakeInvalidPassword = handshakeError("invalid password supplied, check your config") +const ErrHandshakeHashFailure = handshakeError("invalid hash length") +const ErrHandshakeIncorrectPassword = handshakeError("password does not match remote side") + // Gets a base metadata with no keys set, but with the correct version numbers. func version_getBaseMetadata() version_metadata { return version_metadata{ - meta: [4]byte{'m', 'e', 't', 'a'}, - ver: 0, - minorVer: 4, + majorVer: ProtocolVersionMajor, + minorVer: ProtocolVersionMinor, } } -// Gets the length of the metadata for this version, used to know how many bytes to read from the start of a connection. -func version_getMetaLength() (mlen int) { - mlen += 4 // meta - mlen++ // ver, as long as it's < 127, which it is in this version - mlen++ // minorVer, as long as it's < 127, which it is in this version - mlen += ed25519.PublicKeySize // key - return -} - // Encodes version metadata into its wire format. -func (m *version_metadata) encode() []byte { - bs := make([]byte, 0, version_getMetaLength()) - bs = append(bs, m.meta[:]...) - bs = append(bs, m.ver) - bs = append(bs, m.minorVer) - bs = append(bs, m.key[:]...) - if len(bs) != version_getMetaLength() { - panic("Inconsistent metadata length") +func (m *version_metadata) encode(privateKey ed25519.PrivateKey, password []byte) ([]byte, error) { + bs := make([]byte, 0, 64) + bs = append(bs, 'm', 'e', 't', 'a') + bs = append(bs, 0, 0) // Remaining message length + + bs = binary.BigEndian.AppendUint16(bs, metaVersionMajor) + bs = binary.BigEndian.AppendUint16(bs, 2) + bs = binary.BigEndian.AppendUint16(bs, m.majorVer) + + bs = binary.BigEndian.AppendUint16(bs, metaVersionMinor) + bs = binary.BigEndian.AppendUint16(bs, 2) + bs = binary.BigEndian.AppendUint16(bs, m.minorVer) + + bs = binary.BigEndian.AppendUint16(bs, metaPublicKey) + bs = binary.BigEndian.AppendUint16(bs, ed25519.PublicKeySize) + bs = append(bs, m.publicKey[:]...) + + bs = binary.BigEndian.AppendUint16(bs, metaPriority) + bs = binary.BigEndian.AppendUint16(bs, 1) + bs = append(bs, m.priority) + + hasher, err := blake2b.New512(password) + if err != nil { + return nil, err } - return bs + n, err := hasher.Write(m.publicKey) + if err != nil { + return nil, err + } + if n != ed25519.PublicKeySize { + return nil, ErrHandshakeHashFailure + } + hash := hasher.Sum(nil) + bs = append(bs, ed25519.Sign(privateKey, hash)...) + + binary.BigEndian.PutUint16(bs[4:6], uint16(len(bs)-6)) + return bs, nil } // Decodes version metadata from its wire format into the struct. -func (m *version_metadata) decode(bs []byte) bool { - if len(bs) != version_getMetaLength() { - return false +func (m *version_metadata) decode(r io.Reader, password []byte) error { + bh := [6]byte{} + if _, err := io.ReadFull(r, bh[:]); err != nil { + return err } - offset := 0 - offset += copy(m.meta[:], bs[offset:]) - m.ver, offset = bs[offset], offset+1 - m.minorVer, offset = bs[offset], offset+1 - m.key = append([]byte(nil), bs[offset:]...) - return true + meta := [4]byte{'m', 'e', 't', 'a'} + if !bytes.Equal(bh[:4], meta[:]) { + return ErrHandshakeInvalidPreamble + } + hl := binary.BigEndian.Uint16(bh[4:6]) + if hl < ed25519.SignatureSize { + return ErrHandshakeInvalidLength + } + bs := make([]byte, hl) + if _, err := io.ReadFull(r, bs); err != nil { + return err + } + sig := bs[len(bs)-ed25519.SignatureSize:] + bs = bs[:len(bs)-ed25519.SignatureSize] + + for len(bs) >= 4 { + op := binary.BigEndian.Uint16(bs[:2]) + oplen := binary.BigEndian.Uint16(bs[2:4]) + if bs = bs[4:]; len(bs) < int(oplen) { + break + } + switch op { + case metaVersionMajor: + m.majorVer = binary.BigEndian.Uint16(bs[:2]) + + case metaVersionMinor: + m.minorVer = binary.BigEndian.Uint16(bs[:2]) + + case metaPublicKey: + m.publicKey = make(ed25519.PublicKey, ed25519.PublicKeySize) + copy(m.publicKey, bs[:ed25519.PublicKeySize]) + + case metaPriority: + m.priority = bs[0] + } + bs = bs[oplen:] + } + + hasher, err := blake2b.New512(password) + if err != nil { + return ErrHandshakeInvalidPassword + } + n, err := hasher.Write(m.publicKey) + if err != nil || n != ed25519.PublicKeySize { + return ErrHandshakeHashFailure + } + hash := hasher.Sum(nil) + if !ed25519.Verify(m.publicKey, hash, sig) { + return ErrHandshakeIncorrectPassword + } + return nil } // Checks that the "meta" bytes and the version numbers are the expected values. func (m *version_metadata) check() bool { - base := version_getBaseMetadata() - return base.meta == m.meta && base.ver == m.ver && base.minorVer == m.minorVer + switch { + case m.majorVer != ProtocolVersionMajor: + return false + case m.minorVer != ProtocolVersionMinor: + return false + case len(m.publicKey) != ed25519.PublicKeySize: + return false + default: + return true + } } diff --git a/src/core/version_test.go b/src/core/version_test.go new file mode 100644 index 00000000..512c6e59 --- /dev/null +++ b/src/core/version_test.go @@ -0,0 +1,78 @@ +package core + +import ( + "bytes" + "crypto/ed25519" + "reflect" + "testing" +) + +func TestVersionPasswordAuth(t *testing.T) { + for _, tt := range []struct { + password1 []byte // The password on node 1 + password2 []byte // The password on node 2 + allowed bool // Should the connection have been allowed? + }{ + {nil, nil, true}, // Allow: No passwords (both nil) + {nil, []byte(""), true}, // Allow: No passwords (mixed nil and empty string) + {nil, []byte("foo"), false}, // Reject: One node has a password, the other doesn't + {[]byte("foo"), []byte(""), false}, // Reject: One node has a password, the other doesn't + {[]byte("foo"), []byte("foo"), true}, // Allow: Same password + {[]byte("foo"), []byte("bar"), false}, // Reject: Different passwords + } { + pk1, sk1, err := ed25519.GenerateKey(nil) + if err != nil { + t.Fatalf("Node 1 failed to generate key: %s", err) + } + + metadata1 := &version_metadata{ + publicKey: pk1, + } + encoded, err := metadata1.encode(sk1, tt.password1) + if err != nil { + t.Fatalf("Node 1 failed to encode metadata: %s", err) + } + + var decoded version_metadata + if allowed := decoded.decode(bytes.NewBuffer(encoded), tt.password2) == nil; allowed != tt.allowed { + t.Fatalf("Permutation %q -> %q should have been %v but was %v", tt.password1, tt.password2, tt.allowed, allowed) + } + } +} + +func TestVersionRoundtrip(t *testing.T) { + for _, password := range [][]byte{ + nil, []byte(""), []byte("foo"), + } { + for _, test := range []*version_metadata{ + {majorVer: 1}, + {majorVer: 256}, + {majorVer: 2, minorVer: 4}, + {majorVer: 2, minorVer: 257}, + {majorVer: 258, minorVer: 259}, + {majorVer: 3, minorVer: 5, priority: 6}, + {majorVer: 260, minorVer: 261, priority: 7}, + } { + // Generate a random public key for each time, since it is + // a required field. + pk, sk, err := ed25519.GenerateKey(nil) + if err != nil { + t.Fatal(err) + } + + test.publicKey = pk + meta, err := test.encode(sk, password) + if err != nil { + t.Fatal(err) + } + encoded := bytes.NewBuffer(meta) + decoded := &version_metadata{} + if err := decoded.decode(encoded, password); err != nil { + t.Fatalf("failed to decode: %s", err) + } + if !reflect.DeepEqual(test, decoded) { + t.Fatalf("round-trip failed\nwant: %+v\n got: %+v", test, decoded) + } + } + } +} diff --git a/src/defaults/defaults.go b/src/defaults/defaults.go deleted file mode 100644 index 7912fc76..00000000 --- a/src/defaults/defaults.go +++ /dev/null @@ -1,44 +0,0 @@ -package defaults - -import "github.com/yggdrasil-network/yggdrasil-go/src/config" - -type MulticastInterfaceConfig = config.MulticastInterfaceConfig - -// Defines which parameters are expected by default for configuration on a -// specific platform. These values are populated in the relevant defaults_*.go -// for the platform being targeted. They must be set. -type platformDefaultParameters struct { - // Admin socket - DefaultAdminListen string - - // Configuration (used for yggdrasilctl) - DefaultConfigFile string - - // Multicast interfaces - DefaultMulticastInterfaces []MulticastInterfaceConfig - - // TUN/TAP - MaximumIfMTU uint64 - DefaultIfMTU uint64 - DefaultIfName string -} - -// Generates default configuration and returns a pointer to the resulting -// NodeConfig. This is used when outputting the -genconf parameter and also when -// using -autoconf. -func GenerateConfig() *config.NodeConfig { - // Create a node configuration and populate it. - cfg := new(config.NodeConfig) - cfg.NewKeys() - cfg.Listen = []string{} - cfg.AdminListen = GetDefaults().DefaultAdminListen - cfg.Peers = []string{} - cfg.InterfacePeers = map[string][]string{} - cfg.AllowedPublicKeys = []string{} - cfg.MulticastInterfaces = GetDefaults().DefaultMulticastInterfaces - cfg.IfName = GetDefaults().DefaultIfName - cfg.IfMTU = GetDefaults().DefaultIfMTU - cfg.NodeInfoPrivacy = false - - return cfg -} diff --git a/src/ipv6rwc/ipv6rwc.go b/src/ipv6rwc/ipv6rwc.go index bbaa8707..59f4f022 100644 --- a/src/ipv6rwc/ipv6rwc.go +++ b/src/ipv6rwc/ipv6rwc.go @@ -19,12 +19,14 @@ import ( const keyStoreTimeout = 2 * time.Minute +/* // Out-of-band packet types const ( typeKeyDummy = iota // nolint:deadcode,varcheck typeKeyLookup typeKeyResponse ) +*/ type keyArray [ed25519.PublicKeySize]byte @@ -57,10 +59,13 @@ func (k *keyStore) init(c *core.Core) { k.core = c k.address = *address.AddrForKey(k.core.PublicKey()) k.subnet = *address.SubnetForKey(k.core.PublicKey()) - if err := k.core.SetOutOfBandHandler(k.oobHandler); err != nil { + /*if err := k.core.SetOutOfBandHandler(k.oobHandler); err != nil { err = fmt.Errorf("tun.core.SetOutOfBandHander: %w", err) panic(err) - } + }*/ + k.core.SetPathNotify(func(key ed25519.PublicKey) { + k.update(key) + }) k.keyToInfo = make(map[keyArray]*keyInfo) k.addrToInfo = make(map[address.Address]*keyInfo) k.addrBuffer = make(map[address.Address]*buffer) @@ -177,7 +182,8 @@ func (k *keyStore) resetTimeout(info *keyInfo) { }) } -func (k *keyStore) oobHandler(fromKey, toKey ed25519.PublicKey, data []byte) { +/* +func (k *keyStore) oobHandler(fromKey, toKey ed25519.PublicKey, data []byte) { // nolint:unused if len(data) != 1+ed25519.SignatureSize { return } @@ -198,18 +204,26 @@ func (k *keyStore) oobHandler(fromKey, toKey ed25519.PublicKey, data []byte) { } } } +*/ func (k *keyStore) sendKeyLookup(partial ed25519.PublicKey) { - sig := ed25519.Sign(k.core.PrivateKey(), partial[:]) - bs := append([]byte{typeKeyLookup}, sig...) - _ = k.core.SendOutOfBand(partial, bs) + /* + sig := ed25519.Sign(k.core.PrivateKey(), partial[:]) + bs := append([]byte{typeKeyLookup}, sig...) + //_ = k.core.SendOutOfBand(partial, bs) + _ = bs + */ + k.core.SendLookup(partial) } -func (k *keyStore) sendKeyResponse(dest ed25519.PublicKey) { +/* +func (k *keyStore) sendKeyResponse(dest ed25519.PublicKey) { // nolint:unused sig := ed25519.Sign(k.core.PrivateKey(), dest[:]) bs := append([]byte{typeKeyResponse}, sig...) - _ = k.core.SendOutOfBand(dest, bs) + //_ = k.core.SendOutOfBand(dest, bs) + _ = bs } +*/ func (k *keyStore) readPC(p []byte) (int, error) { buf := make([]byte, k.core.MTU(), 65535) diff --git a/src/multicast/admin.go b/src/multicast/admin.go index 2ae6ec08..0042f519 100644 --- a/src/multicast/admin.go +++ b/src/multicast/admin.go @@ -2,33 +2,63 @@ package multicast import ( "encoding/json" + "slices" + "strings" + "github.com/Arceliar/phony" "github.com/yggdrasil-network/yggdrasil-go/src/admin" ) type GetMulticastInterfacesRequest struct{} type GetMulticastInterfacesResponse struct { - Interfaces []string `json:"multicast_interfaces"` + Interfaces []MulticastInterfaceState `json:"multicast_interfaces"` } -func (m *Multicast) getMulticastInterfacesHandler(req *GetMulticastInterfacesRequest, res *GetMulticastInterfacesResponse) error { - res.Interfaces = []string{} - for _, v := range m.Interfaces() { - res.Interfaces = append(res.Interfaces, v.Name) - } +type MulticastInterfaceState struct { + Name string `json:"name"` + Address string `json:"address"` + Beacon bool `json:"beacon"` + Listen bool `json:"listen"` + Password bool `json:"password"` +} + +func (m *Multicast) getMulticastInterfacesHandler(_ *GetMulticastInterfacesRequest, res *GetMulticastInterfacesResponse) error { + res.Interfaces = []MulticastInterfaceState{} + phony.Block(m, func() { + for name, intf := range m._interfaces { + is := MulticastInterfaceState{ + Name: intf.iface.Name, + Beacon: intf.beacon, + Listen: intf.listen, + Password: len(intf.password) > 0, + } + if li := m._listeners[name]; li != nil && li.listener != nil { + is.Address = li.listener.Addr().String() + } else { + is.Address = "-" + } + res.Interfaces = append(res.Interfaces, is) + } + }) + slices.SortStableFunc(res.Interfaces, func(a, b MulticastInterfaceState) int { + return strings.Compare(a.Name, b.Name) + }) return nil } func (m *Multicast) SetupAdminHandlers(a *admin.AdminSocket) { - _ = a.AddHandler("getMulticastInterfaces", []string{}, func(in json.RawMessage) (interface{}, error) { - req := &GetMulticastInterfacesRequest{} - res := &GetMulticastInterfacesResponse{} - if err := json.Unmarshal(in, &req); err != nil { - return nil, err - } - if err := m.getMulticastInterfacesHandler(req, res); err != nil { - return nil, err - } - return res, nil - }) + _ = a.AddHandler( + "getMulticastInterfaces", "Show which interfaces multicast is enabled on", []string{}, + func(in json.RawMessage) (interface{}, error) { + req := &GetMulticastInterfacesRequest{} + res := &GetMulticastInterfacesResponse{} + if err := json.Unmarshal(in, &req); err != nil { + return nil, err + } + if err := m.getMulticastInterfacesHandler(req, res); err != nil { + return nil, err + } + return res, nil + }, + ) } diff --git a/src/multicast/advertisement.go b/src/multicast/advertisement.go new file mode 100644 index 00000000..d0db8b5a --- /dev/null +++ b/src/multicast/advertisement.go @@ -0,0 +1,39 @@ +package multicast + +import ( + "crypto/ed25519" + "encoding/binary" + "fmt" +) + +type multicastAdvertisement struct { + MajorVersion uint16 + MinorVersion uint16 + PublicKey ed25519.PublicKey + Port uint16 + Hash []byte +} + +func (m *multicastAdvertisement) MarshalBinary() ([]byte, error) { + b := make([]byte, 0, ed25519.PublicKeySize+8+len(m.Hash)) + b = binary.BigEndian.AppendUint16(b, m.MajorVersion) + b = binary.BigEndian.AppendUint16(b, m.MinorVersion) + b = append(b, m.PublicKey...) + b = binary.BigEndian.AppendUint16(b, m.Port) + b = binary.BigEndian.AppendUint16(b, uint16(len(m.Hash))) + b = append(b, m.Hash...) + return b, nil +} + +func (m *multicastAdvertisement) UnmarshalBinary(b []byte) error { + if len(b) < ed25519.PublicKeySize+8 { + return fmt.Errorf("invalid multicast beacon") + } + m.MajorVersion = binary.BigEndian.Uint16(b[0:2]) + m.MinorVersion = binary.BigEndian.Uint16(b[2:4]) + m.PublicKey = append(m.PublicKey[:0], b[4:4+ed25519.PublicKeySize]...) + m.Port = binary.BigEndian.Uint16(b[4+ed25519.PublicKeySize : 6+ed25519.PublicKeySize]) + dl := binary.BigEndian.Uint16(b[6+ed25519.PublicKeySize : 8+ed25519.PublicKeySize]) + m.Hash = append(m.Hash[:0], b[8+ed25519.PublicKeySize:8+ed25519.PublicKeySize+dl]...) + return nil +} diff --git a/src/multicast/advertisement_test.go b/src/multicast/advertisement_test.go new file mode 100644 index 00000000..9541da60 --- /dev/null +++ b/src/multicast/advertisement_test.go @@ -0,0 +1,38 @@ +package multicast + +import ( + "crypto/ed25519" + "reflect" + "testing" +) + +func TestMulticastAdvertisementRoundTrip(t *testing.T) { + pk, sk, err := ed25519.GenerateKey(nil) + if err != nil { + t.Fatal(err) + } + + orig := multicastAdvertisement{ + MajorVersion: 1, + MinorVersion: 2, + PublicKey: pk, + Port: 3, + Hash: sk, // any bytes will do + } + + ob, err := orig.MarshalBinary() + if err != nil { + t.Fatal(err) + } + + var new multicastAdvertisement + if err := new.UnmarshalBinary(ob); err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(orig, new) { + t.Logf("original: %+v", orig) + t.Logf("new: %+v", new) + t.Fatalf("differences found after round-trip") + } +} diff --git a/src/multicast/multicast.go b/src/multicast/multicast.go index 9093e4cf..9134501c 100644 --- a/src/multicast/multicast.go +++ b/src/multicast/multicast.go @@ -4,19 +4,20 @@ import ( "bytes" "context" "crypto/ed25519" - "encoding/binary" "encoding/hex" "fmt" + "math/rand" "net" "net/url" - "regexp" + "sync/atomic" "time" "github.com/Arceliar/phony" "github.com/gologme/log" + "github.com/wlynxg/anet" - "github.com/yggdrasil-network/yggdrasil-go/src/config" "github.com/yggdrasil-network/yggdrasil-go/src/core" + "golang.org/x/crypto/blake2b" "golang.org/x/net/ipv6" ) @@ -27,65 +28,75 @@ import ( type Multicast struct { phony.Inbox core *core.Core - config *config.NodeConfig log *log.Logger sock *ipv6.PacketConn - groupAddr string - listeners map[string]*listenerInfo - isOpen bool - _interfaces map[string]interfaceInfo + running atomic.Bool + _listeners map[string]*listenerInfo + _interfaces map[string]*interfaceInfo + _timer *time.Timer + config struct { + _groupAddr GroupAddress + _interfaces map[MulticastInterface]struct{} + } } type interfaceInfo struct { - iface net.Interface - addrs []net.Addr - beacon bool - listen bool - port uint16 + iface net.Interface + addrs []net.Addr + beacon bool + listen bool + port uint16 + priority uint8 + password []byte + hash []byte } type listenerInfo struct { - listener *core.TcpListener + listener *core.Listener time time.Time interval time.Duration port uint16 } -// Init prepares the multicast interface for use. -func (m *Multicast) Init(core *core.Core, nc *config.NodeConfig, log *log.Logger, options interface{}) error { - m.core = core - m.config = nc - m.log = log - m.listeners = make(map[string]*listenerInfo) - m._interfaces = make(map[string]interfaceInfo) - m.groupAddr = "[ff02::114]:9001" - return nil -} - // Start starts the multicast interface. This launches goroutines which will // listen for multicast beacons from other hosts and will advertise multicast // beacons out to the network. -func (m *Multicast) Start() error { +func New(core *core.Core, log *log.Logger, opts ...SetupOption) (*Multicast, error) { + m := &Multicast{ + core: core, + log: log, + _listeners: make(map[string]*listenerInfo), + _interfaces: make(map[string]*interfaceInfo), + } + m.config._interfaces = map[MulticastInterface]struct{}{} + m.config._groupAddr = GroupAddress("[ff02::114]:9001") + for _, opt := range opts { + m._applyOption(opt) + } var err error phony.Block(m, func() { err = m._start() }) - m.log.Debugln("Started multicast module") - return err + return m, err } func (m *Multicast) _start() error { - if m.isOpen { + if !m.running.CompareAndSwap(false, true) { return fmt.Errorf("multicast module is already started") } - m.config.RLock() - defer m.config.RUnlock() - if len(m.config.MulticastInterfaces) == 0 { + var anyEnabled bool + for intf := range m.config._interfaces { + anyEnabled = anyEnabled || intf.Beacon || intf.Listen + } + if !anyEnabled { + m.running.Store(false) return nil } - m.log.Infoln("Starting multicast module") - addr, err := net.ResolveUDPAddr("udp", m.groupAddr) + m.log.Debugln("Starting multicast module") + defer m.log.Debugln("Started multicast module") + addr, err := net.ResolveUDPAddr("udp", string(m.config._groupAddr)) if err != nil { + m.running.Store(false) return err } listenString := fmt.Sprintf("[::]:%v", addr.Port) @@ -94,6 +105,7 @@ func (m *Multicast) _start() error { } conn, err := lc.ListenPacket(context.Background(), "udp6", listenString) if err != nil { + m.running.Store(false) return err } m.sock = ipv6.NewPacketConn(conn) @@ -101,7 +113,6 @@ func (m *Multicast) _start() error { // Windows can't set this flag, so we need to handle it in other ways } - m.isOpen = true go m.listen() m.Act(nil, m._multicastStarted) m.Act(nil, m._announce) @@ -111,11 +122,7 @@ func (m *Multicast) _start() error { // IsStarted returns true if the module has been started. func (m *Multicast) IsStarted() bool { - var isOpen bool - phony.Block(m, func() { - isOpen = m.isOpen - }) - return isOpen + return m.running.Load() } // Stop stops the multicast module. @@ -129,8 +136,10 @@ func (m *Multicast) Stop() error { } func (m *Multicast) _stop() error { + if !m.running.CompareAndSwap(true, false) { + return nil + } m.log.Infoln("Stopping multicast module") - m.isOpen = false if m.sock != nil { m.sock.Close() } @@ -138,16 +147,24 @@ func (m *Multicast) _stop() error { } func (m *Multicast) _updateInterfaces() { - interfaces := m.getAllowedInterfaces() + interfaces := m._getAllowedInterfaces() for name, info := range interfaces { - addrs, err := info.iface.Addrs() + // 'anet' package is used here to avoid https://github.com/golang/go/issues/40569 + addrs, err := anet.InterfaceAddrsByInterface(&info.iface) if err != nil { m.log.Warnf("Failed up get addresses for interface %s: %s", name, err) delete(interfaces, name) continue } - info.addrs = addrs + for _, addr := range addrs { + addrIP, _, err := net.ParseCIDR(addr.String()) + if err != nil || addrIP.To4() != nil || !addrIP.IsLinkLocalUnicast() { + continue + } + info.addrs = append(info.addrs, addr) + } interfaces[name] = info + m.log.Debugf("Discovered addresses for interface %s: %s", name, addrs) } m._interfaces = interfaces } @@ -163,75 +180,91 @@ func (m *Multicast) Interfaces() map[string]net.Interface { } // getAllowedInterfaces returns the currently known/enabled multicast interfaces. -func (m *Multicast) getAllowedInterfaces() map[string]interfaceInfo { - interfaces := make(map[string]interfaceInfo) - // Get interface expressions from config - ifcfgs := m.config.MulticastInterfaces +func (m *Multicast) _getAllowedInterfaces() map[string]*interfaceInfo { + interfaces := make(map[string]*interfaceInfo) // Ask the system for network interfaces - allifaces, err := net.Interfaces() + // 'anet' package is used here to avoid https://github.com/golang/go/issues/40569 + allifaces, err := anet.Interfaces() if err != nil { // Don't panic, since this may be from e.g. too many open files (from too much connection spam) - // TODO? log something + m.log.Debugf("Failed to get interfaces: %s", err) return nil } // Work out which interfaces to announce on + pk := m.core.PublicKey() for _, iface := range allifaces { - if iface.Flags&net.FlagUp == 0 { - // Ignore interfaces that are down - continue + switch { + case iface.Flags&net.FlagUp == 0: + continue // Ignore interfaces that are down + case iface.Flags&net.FlagRunning == 0: + continue // Ignore interfaces that are not running + case iface.Flags&net.FlagMulticast == 0: + continue // Ignore non-multicast interfaces + case iface.Flags&net.FlagPointToPoint != 0: + continue // Ignore point-to-point interfaces } - if iface.Flags&net.FlagMulticast == 0 { - // Ignore non-multicast interfaces - continue - } - if iface.Flags&net.FlagPointToPoint != 0 { - // Ignore point-to-point interfaces - continue - } - for _, ifcfg := range ifcfgs { + for ifcfg := range m.config._interfaces { // Compile each regular expression - e, err := regexp.Compile(ifcfg.Regex) - if err != nil { - panic(err) - } // Does the interface match the regular expression? Store it if so - if e.MatchString(iface.Name) { - if ifcfg.Beacon || ifcfg.Listen { - info := interfaceInfo{ - iface: iface, - beacon: ifcfg.Beacon, - listen: ifcfg.Listen, - port: ifcfg.Port, - } - interfaces[iface.Name] = info - } - break + if !ifcfg.Beacon && !ifcfg.Listen { + continue } + if !ifcfg.Regex.MatchString(iface.Name) { + continue + } + hasher, err := blake2b.New512([]byte(ifcfg.Password)) + if err != nil { + continue + } + if n, err := hasher.Write(pk); err != nil { + continue + } else if n != ed25519.PublicKeySize { + continue + } + interfaces[iface.Name] = &interfaceInfo{ + iface: iface, + beacon: ifcfg.Beacon, + listen: ifcfg.Listen, + port: ifcfg.Port, + priority: ifcfg.Priority, + password: []byte(ifcfg.Password), + hash: hasher.Sum(nil), + } + break } } return interfaces } +func (m *Multicast) AnnounceNow() { + phony.Block(m, func() { + if m._timer != nil && !m._timer.Stop() { + <-m._timer.C + } + m.Act(nil, m._announce) + }) +} + func (m *Multicast) _announce() { - if !m.isOpen { + if !m.running.Load() { return } m._updateInterfaces() - groupAddr, err := net.ResolveUDPAddr("udp6", m.groupAddr) + groupAddr, err := net.ResolveUDPAddr("udp6", string(m.config._groupAddr)) if err != nil { panic(err) } - destAddr, err := net.ResolveUDPAddr("udp6", m.groupAddr) + destAddr, err := net.ResolveUDPAddr("udp6", string(m.config._groupAddr)) if err != nil { panic(err) } // There might be interfaces that we configured listeners for but are no // longer up - if that's the case then we should stop the listeners - for name, info := range m.listeners { + for name, info := range m._listeners { // Prepare our stop function! stop := func() { - info.listener.Stop() - delete(m.listeners, name) + info.listener.Cancel() + delete(m._listeners, name) m.log.Debugln("No longer multicasting on", name) } // If the interface is no longer visible on the system then stop the @@ -243,7 +276,7 @@ func (m *Multicast) _announce() { // It's possible that the link-local listener address has changed so if // that is the case then we should clean up the interface listener found := false - listenaddr, err := net.ResolveTCPAddr("tcp6", info.listener.Listener.Addr().String()) + listenaddr, err := net.ResolveTCPAddr("tcp6", info.listener.Addr().String()) if err != nil { stop() continue @@ -272,13 +305,9 @@ func (m *Multicast) _announce() { for _, info := range m._interfaces { iface := info.iface for _, addr := range info.addrs { - addrIP, _, _ := net.ParseCIDR(addr.String()) - // Ignore IPv4 addresses - if addrIP.To4() != nil { - continue - } - // Ignore non-link-local addresses - if !addrIP.IsLinkLocalUnicast() { + addrIP, _, err := net.ParseCIDR(addr.String()) + // Ignore IPv4 addresses or non-link-local addresses + if err != nil || addrIP.To4() != nil || !addrIP.IsLinkLocalUnicast() { continue } if info.listen { @@ -290,24 +319,27 @@ func (m *Multicast) _announce() { } // Try and see if we already have a TCP listener for this interface var linfo *listenerInfo - if nfo, ok := m.listeners[iface.Name]; !ok || nfo.listener.Listener == nil { + if _, ok := m._listeners[iface.Name]; !ok { // No listener was found - let's create one - urlString := fmt.Sprintf("tls://[%s]:%d", addrIP, info.port) - u, err := url.Parse(urlString) - if err != nil { - panic(err) + v := &url.Values{} + v.Add("priority", fmt.Sprintf("%d", info.priority)) + v.Add("password", string(info.password)) + u := &url.URL{ + Scheme: "tls", + Host: net.JoinHostPort(addrIP.String(), fmt.Sprintf("%d", info.port)), + RawQuery: v.Encode(), } - if li, err := m.core.Listen(u, iface.Name); err == nil { + if li, err := m.core.ListenLocal(u, iface.Name); err == nil { m.log.Debugln("Started multicasting on", iface.Name) // Store the listener so that we can stop it later if needed linfo = &listenerInfo{listener: li, time: time.Now(), port: info.port} - m.listeners[iface.Name] = linfo + m._listeners[iface.Name] = linfo } else { m.log.Warnln("Not multicasting on", iface.Name, "due to error:", err) } } else { // An existing listener was found - linfo = m.listeners[iface.Name] + linfo = m._listeners[iface.Name] } // Make sure nothing above failed for some reason if linfo == nil { @@ -316,37 +348,47 @@ func (m *Multicast) _announce() { if time.Since(linfo.time) < linfo.interval { continue } - // Get the listener details and construct the multicast beacon - lladdr := linfo.listener.Listener.Addr().String() - if a, err := net.ResolveTCPAddr("tcp6", lladdr); err == nil { - a.Zone = "" - destAddr.Zone = iface.Name - msg := append([]byte(nil), m.core.GetSelf().Key...) - msg = append(msg, a.IP...) - pbs := make([]byte, 2) - binary.BigEndian.PutUint16(pbs, uint16(a.Port)) - msg = append(msg, pbs...) - _, _ = m.sock.WriteTo(msg, nil, destAddr) + addr := linfo.listener.Addr().(*net.TCPAddr) + adv := multicastAdvertisement{ + MajorVersion: core.ProtocolVersionMajor, + MinorVersion: core.ProtocolVersionMinor, + PublicKey: m.core.PublicKey(), + Port: uint16(addr.Port), + Hash: info.hash, + } + msg, err := adv.MarshalBinary() + if err != nil { + continue + } + destAddr.Zone = iface.Name + if _, err = m.sock.WriteTo(msg, nil, destAddr); err != nil { + m.log.Warn("Failed to send multicast beacon:", err) } if linfo.interval.Seconds() < 15 { linfo.interval += time.Second } + linfo.time = time.Now() break } } - time.AfterFunc(time.Second, func() { + annInterval := time.Second + time.Microsecond*(time.Duration(rand.Intn(1048576))) // Randomize delay + m._timer = time.AfterFunc(annInterval, func() { m.Act(nil, m._announce) }) } func (m *Multicast) listen() { - groupAddr, err := net.ResolveUDPAddr("udp6", m.groupAddr) + groupAddr, err := net.ResolveUDPAddr("udp6", string(m.config._groupAddr)) if err != nil { panic(err) } bs := make([]byte, 2048) + hb := make([]byte, 0, blake2b.Size) // Reused to reduce hash allocations for { - nBytes, rcm, fromAddr, err := m.sock.ReadFrom(bs) + if !m.running.Load() { + return + } + n, rcm, fromAddr, err := m.sock.ReadFrom(bs) if err != nil { if !m.IsStarted() { return @@ -364,40 +406,45 @@ func (m *Multicast) listen() { continue } } - if nBytes < ed25519.PublicKeySize { + var adv multicastAdvertisement + if err := adv.UnmarshalBinary(bs[:n]); err != nil { continue } - var key ed25519.PublicKey - key = append(key, bs[:ed25519.PublicKeySize]...) - if bytes.Equal(key, m.core.GetSelf().Key) { - continue // don't bother trying to peer with self - } - begin := ed25519.PublicKeySize - end := nBytes - 2 - if end <= begin { - continue // malformed address - } - ip := bs[begin:end] - port := binary.BigEndian.Uint16(bs[end:nBytes]) - anAddr := net.TCPAddr{IP: ip, Port: int(port)} - addr, err := net.ResolveTCPAddr("tcp6", anAddr.String()) - if err != nil { + switch { + case adv.MajorVersion != core.ProtocolVersionMajor: + continue + case adv.MinorVersion != core.ProtocolVersionMinor: + continue + case adv.PublicKey.Equal(m.core.PublicKey()): continue } from := fromAddr.(*net.UDPAddr) - if !from.IP.Equal(addr.IP) { - continue - } - var interfaces map[string]interfaceInfo + from.Port = int(adv.Port) + var interfaces map[string]*interfaceInfo phony.Block(m, func() { interfaces = m._interfaces }) if info, ok := interfaces[from.Zone]; ok && info.listen { - addr.Zone = "" - pin := fmt.Sprintf("/?key=%s", hex.EncodeToString(key)) - u, err := url.Parse("tls://" + addr.String() + pin) + hasher, err := blake2b.New512(info.password) if err != nil { - m.log.Debugln("Call from multicast failed, parse error:", addr.String(), err) + continue + } + if n, err := hasher.Write(adv.PublicKey); err != nil { + continue + } else if n != ed25519.PublicKeySize { + continue + } + if !bytes.Equal(hasher.Sum(hb[:0]), adv.Hash) { + continue + } + v := &url.Values{} + v.Add("key", hex.EncodeToString(adv.PublicKey)) + v.Add("priority", fmt.Sprintf("%d", info.priority)) + v.Add("password", string(info.password)) + u := &url.URL{ + Scheme: "tls", + Host: from.String(), + RawQuery: v.Encode(), } if err := m.core.CallPeer(u, from.Zone); err != nil { m.log.Debugln("Call from multicast failed:", err) diff --git a/src/multicast/multicast_darwin_cgo.go b/src/multicast/multicast_darwin_cgo.go index b7d7358c..16266381 100644 --- a/src/multicast/multicast_darwin_cgo.go +++ b/src/multicast/multicast_darwin_cgo.go @@ -31,7 +31,7 @@ import ( ) func (m *Multicast) _multicastStarted() { - if !m.isOpen { + if !m.running.Load() { return } C.StopAWDLBrowsing() diff --git a/src/multicast/multicast_unix.go b/src/multicast/multicast_unix.go index c59d876b..08230735 100644 --- a/src/multicast/multicast_unix.go +++ b/src/multicast/multicast_unix.go @@ -15,15 +15,19 @@ func (m *Multicast) _multicastStarted() { func (m *Multicast) multicastReuse(network string, address string, c syscall.RawConn) error { var control error - var reuseport error + var reuseaddr error control = c.Control(func(fd uintptr) { - reuseport = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) + // Previously we used SO_REUSEPORT here, but that meant that machines running + // Yggdrasil nodes as different users would inevitably fail with EADDRINUSE. + // The behaviour for multicast is similar with both, so we'll use SO_REUSEADDR + // instead. + reuseaddr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) }) switch { - case reuseport != nil: - return reuseport + case reuseaddr != nil: + return reuseaddr default: return control } diff --git a/src/multicast/options.go b/src/multicast/options.go new file mode 100644 index 00000000..bd9fea5a --- /dev/null +++ b/src/multicast/options.go @@ -0,0 +1,30 @@ +package multicast + +import "regexp" + +func (m *Multicast) _applyOption(opt SetupOption) { + switch v := opt.(type) { + case MulticastInterface: + m.config._interfaces[v] = struct{}{} + case GroupAddress: + m.config._groupAddr = v + } +} + +type SetupOption interface { + isSetupOption() +} + +type MulticastInterface struct { + Regex *regexp.Regexp + Beacon bool + Listen bool + Port uint16 + Priority uint8 + Password string +} + +type GroupAddress string + +func (a MulticastInterface) isSetupOption() {} +func (a GroupAddress) isSetupOption() {} diff --git a/src/tun/admin.go b/src/tun/admin.go new file mode 100644 index 00000000..90f712e9 --- /dev/null +++ b/src/tun/admin.go @@ -0,0 +1,45 @@ +package tun + +import ( + "encoding/json" + + "github.com/yggdrasil-network/yggdrasil-go/src/admin" +) + +type GetTUNRequest struct{} +type GetTUNResponse struct { + Enabled bool `json:"enabled"` + Name string `json:"name,omitempty"` + MTU uint64 `json:"mtu,omitempty"` +} + +type TUNEntry struct { + MTU uint64 `json:"mtu"` +} + +func (t *TunAdapter) getTUNHandler(req *GetTUNRequest, res *GetTUNResponse) error { + res.Enabled = t.isEnabled + if !t.isEnabled { + return nil + } + res.Name = t.Name() + res.MTU = t.MTU() + return nil +} + +func (t *TunAdapter) SetupAdminHandlers(a *admin.AdminSocket) { + _ = a.AddHandler( + "getTun", "Show information about the node's TUN interface", []string{}, + func(in json.RawMessage) (interface{}, error) { + req := &GetTUNRequest{} + res := &GetTUNResponse{} + if err := json.Unmarshal(in, &req); err != nil { + return nil, err + } + if err := t.getTUNHandler(req, res); err != nil { + return nil, err + } + return res, nil + }, + ) +} diff --git a/src/tun/iface.go b/src/tun/iface.go new file mode 100644 index 00000000..f1898281 --- /dev/null +++ b/src/tun/iface.go @@ -0,0 +1,75 @@ +package tun + +import ( + "errors" + + wgtun "golang.zx2c4.com/wireguard/tun" +) + +const TUN_OFFSET_BYTES = 80 // sizeof(virtio_net_hdr) + +func (tun *TunAdapter) read() { + vs := tun.iface.BatchSize() + bufs := make([][]byte, vs) + sizes := make([]int, vs) + for i := range bufs { + bufs[i] = make([]byte, TUN_OFFSET_BYTES+65535) + } + for { + n, err := tun.iface.Read(bufs, sizes, TUN_OFFSET_BYTES) + if err != nil { + if errors.Is(err, wgtun.ErrTooManySegments) { + tun.log.Debugln("TUN segments dropped: %v", err) + continue + } + tun.log.Errorln("Error reading TUN:", err) + return + } + for i, b := range bufs[:n] { + if _, err := tun.rwc.Write(b[TUN_OFFSET_BYTES : TUN_OFFSET_BYTES+sizes[i]]); err != nil { + tun.log.Debugln("Unable to send packet:", err) + } + } + } +} + +func (tun *TunAdapter) queue() { + for { + p := bufPool.Get().([]byte)[:bufPoolSize] + n, err := tun.rwc.Read(p) + if err != nil { + tun.log.Errorln("Exiting TUN writer due to core read error:", err) + return + } + tun.ch <- p[:n] + } +} + +func (tun *TunAdapter) write() { + vs := cap(tun.ch) + bufs := make([][]byte, vs) + for i := range bufs { + bufs[i] = make([]byte, TUN_OFFSET_BYTES+65535) + } + for { + n := len(tun.ch) + if n == 0 { + n = 1 // Nothing queued up yet, wait for it instead + } + for i := 0; i < n; i++ { + msg := <-tun.ch + bufs[i] = append(bufs[i][:TUN_OFFSET_BYTES], msg...) + bufPool.Put(msg) // nolint:staticcheck + } + if !tun.isEnabled { + continue // Nothing to do, the tun isn't enabled + } + if _, err := tun.iface.Write(bufs[:n], TUN_OFFSET_BYTES); err != nil { + tun.Act(nil, func() { + if !tun.isOpen { + tun.log.Errorln("TUN iface write error:", err) + } + }) + } + } +} diff --git a/src/tun/options.go b/src/tun/options.go new file mode 100644 index 00000000..58d3d80c --- /dev/null +++ b/src/tun/options.go @@ -0,0 +1,24 @@ +package tun + +func (m *TunAdapter) _applyOption(opt SetupOption) { + switch v := opt.(type) { + case InterfaceName: + m.config.name = v + case InterfaceMTU: + m.config.mtu = v + case FileDescriptor: + m.config.fd = int32(v) + } +} + +type SetupOption interface { + isSetupOption() +} + +type InterfaceName string +type InterfaceMTU uint64 +type FileDescriptor int32 + +func (a InterfaceName) isSetupOption() {} +func (a InterfaceMTU) isSetupOption() {} +func (a FileDescriptor) isSetupOption() {} diff --git a/src/tuntap/tun.go b/src/tun/tun.go similarity index 68% rename from src/tuntap/tun.go rename to src/tun/tun.go index eddccbcd..d15a0674 100644 --- a/src/tuntap/tun.go +++ b/src/tun/tun.go @@ -1,52 +1,59 @@ -package tuntap +package tun // This manages the tun driver to send/recv packets to/from applications -// TODO: Crypto-key routing support -// TODO: Set MTU of session properly -// TODO: Reject packets that exceed session MTU with ICMPv6 for PMTU Discovery // TODO: Connection timeouts (call Conn.Close() when we want to time out) // TODO: Don't block in reader on writes that are pending searches import ( "errors" "fmt" + "io" "net" - - //"sync" + "sync" "github.com/Arceliar/phony" - "github.com/gologme/log" - "golang.zx2c4.com/wireguard/tun" + wgtun "golang.zx2c4.com/wireguard/tun" "github.com/yggdrasil-network/yggdrasil-go/src/address" "github.com/yggdrasil-network/yggdrasil-go/src/config" - "github.com/yggdrasil-network/yggdrasil-go/src/defaults" - "github.com/yggdrasil-network/yggdrasil-go/src/ipv6rwc" + "github.com/yggdrasil-network/yggdrasil-go/src/core" ) type MTU uint16 +type ReadWriteCloser interface { + io.ReadWriteCloser + Address() address.Address + Subnet() address.Subnet + MaxMTU() uint64 + SetMTU(uint64) +} + // TunAdapter represents a running TUN interface and extends the // yggdrasil.Adapter type. In order to use the TUN adapter with Yggdrasil, you // should pass this object to the yggdrasil.SetRouterAdapter() function before // calling yggdrasil.Start(). type TunAdapter struct { - rwc *ipv6rwc.ReadWriteCloser - config *config.NodeConfig - log *log.Logger + rwc ReadWriteCloser + log core.Logger addr address.Address subnet address.Subnet mtu uint64 - iface tun.Device + iface wgtun.Device phony.Inbox // Currently only used for _handlePacket from the reader, TODO: all the stuff that currently needs a mutex below - //mutex sync.RWMutex // Protects the below - isOpen bool - isEnabled bool // Used by the writer to drop sessionTraffic if not enabled + isOpen bool + isEnabled bool // Used by the writer to drop sessionTraffic if not enabled + config struct { + fd int32 + name InterfaceName + mtu InterfaceMTU + } + ch chan []byte } // Gets the maximum supported MTU for the platform based on the defaults in -// defaults.GetDefaults(). +// config.GetDefaults(). func getSupportedMTU(mtu uint64) uint64 { if mtu < 1280 { return 1280 @@ -75,72 +82,74 @@ func (tun *TunAdapter) MTU() uint64 { // DefaultName gets the default TUN interface name for your platform. func DefaultName() string { - return defaults.GetDefaults().DefaultIfName + return config.GetDefaults().DefaultIfName } // DefaultMTU gets the default TUN interface MTU for your platform. This can // be as high as MaximumMTU(), depending on platform, but is never lower than 1280. func DefaultMTU() uint64 { - return defaults.GetDefaults().DefaultIfMTU + return config.GetDefaults().DefaultIfMTU } // MaximumMTU returns the maximum supported TUN interface MTU for your // platform. This can be as high as 65535, depending on platform, but is never // lower than 1280. func MaximumMTU() uint64 { - return defaults.GetDefaults().MaximumIfMTU + return config.GetDefaults().MaximumIfMTU } // Init initialises the TUN module. You must have acquired a Listener from // the Yggdrasil core before this point and it must not be in use elsewhere. -func (tun *TunAdapter) Init(rwc *ipv6rwc.ReadWriteCloser, config *config.NodeConfig, log *log.Logger, options interface{}) error { - tun.rwc = rwc - tun.config = config - tun.log = log - return nil -} - -// Start the setup process for the TUN adapter. If successful, starts the -// reader actor to handle packets on that interface. -func (tun *TunAdapter) Start() error { - var err error - phony.Block(tun, func() { - err = tun._start() - }) - return err +func New(rwc ReadWriteCloser, log core.Logger, opts ...SetupOption) (*TunAdapter, error) { + tun := &TunAdapter{ + rwc: rwc, + log: log, + } + for _, opt := range opts { + tun._applyOption(opt) + } + return tun, tun._start() } func (tun *TunAdapter) _start() error { if tun.isOpen { return errors.New("TUN module is already started") } - if tun.config == nil { - return errors.New("no configuration available to TUN") - } - tun.config.RLock() - defer tun.config.RUnlock() tun.addr = tun.rwc.Address() tun.subnet = tun.rwc.Subnet() - addr := fmt.Sprintf("%s/%d", net.IP(tun.addr[:]).String(), 8*len(address.GetPrefix())-1) - if tun.config.IfName == "none" || tun.config.IfName == "dummy" { + prefix := address.GetPrefix() + var addr string + if tun.addr.IsValid() { + addr = fmt.Sprintf("%s/%d", net.IP(tun.addr[:]).String(), 8*len(prefix[:])-1) + } + if tun.config.name == "none" || tun.config.name == "dummy" { tun.log.Debugln("Not starting TUN as ifname is none or dummy") tun.isEnabled = false + go tun.queue() go tun.write() return nil } - mtu := tun.config.IfMTU + mtu := uint64(tun.config.mtu) if tun.rwc.MaxMTU() < mtu { mtu = tun.rwc.MaxMTU() } - if err := tun.setup(tun.config.IfName, addr, mtu); err != nil { + var err error + if tun.config.fd > 0 { + err = tun.setupFD(tun.config.fd, addr, mtu) + } else { + err = tun.setup(string(tun.config.name), addr, mtu) + } + if err != nil { return err } if tun.MTU() != mtu { - tun.log.Warnf("Warning: Interface MTU %d automatically adjusted to %d (supported range is 1280-%d)", tun.config.IfMTU, tun.MTU(), MaximumMTU()) + tun.log.Warnf("Warning: Interface MTU %d automatically adjusted to %d (supported range is 1280-%d)", tun.config.mtu, tun.MTU(), MaximumMTU()) } tun.rwc.SetMTU(tun.MTU()) tun.isOpen = true tun.isEnabled = true + tun.ch = make(chan []byte, tun.iface.BatchSize()) + go tun.queue() go tun.read() go tun.write() return nil @@ -174,3 +183,12 @@ func (tun *TunAdapter) _stop() error { } return nil } + +const bufPoolSize = TUN_OFFSET_BYTES + 65535 + +var bufPool = sync.Pool{ + New: func() any { + b := [bufPoolSize]byte{} + return b[:] + }, +} diff --git a/src/tuntap/tun_darwin.go b/src/tun/tun_darwin.go similarity index 73% rename from src/tuntap/tun_darwin.go rename to src/tun/tun_darwin.go index 6f6e2528..deeb115e 100644 --- a/src/tuntap/tun_darwin.go +++ b/src/tun/tun_darwin.go @@ -1,12 +1,14 @@ -//go:build !mobile -// +build !mobile +//go:build darwin || ios +// +build darwin ios -package tuntap +package tun // The darwin platform specific tun parts import ( "encoding/binary" + "fmt" + "os" "strconv" "strings" "unsafe" @@ -23,7 +25,7 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error { } iface, err := wgtun.CreateTUN(ifname, int(mtu)) if err != nil { - panic(err) + return fmt.Errorf("failed to create TUN: %w", err) } tun.iface = iface if m, err := iface.MTU(); err == nil { @@ -31,7 +33,35 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error { } else { tun.mtu = 0 } - return tun.setupAddress(addr) + if addr != "" { + return tun.setupAddress(addr) + } + return nil +} + +// Configures the "utun" adapter from an existing file descriptor. +func (tun *TunAdapter) setupFD(fd int32, addr string, mtu uint64) error { + dfd, err := unix.Dup(int(fd)) + if err != nil { + return fmt.Errorf("failed to duplicate FD: %w", err) + } + err = unix.SetNonblock(dfd, true) + if err != nil { + unix.Close(dfd) + return fmt.Errorf("failed to set FD as non-blocking: %w", err) + } + iface, err := wgtun.CreateTUNFromFile(os.NewFile(uintptr(dfd), "/dev/tun"), 0) + if err != nil { + unix.Close(dfd) + return fmt.Errorf("failed to create TUN from FD: %w", err) + } + tun.iface = iface + if m, err := iface.MTU(); err == nil { + tun.mtu = getSupportedMTU(uint64(m)) + } else { + tun.mtu = 0 + } + return nil // tun.setupAddress(addr) } const ( @@ -81,8 +111,8 @@ func (tun *TunAdapter) setupAddress(addr string) error { var err error if fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, 0); err != nil { - tun.log.Printf("Create AF_SYSTEM socket failed: %v.", err) - return err + tun.log.Errorf("Create AF_SYSTEM socket failed: %v.", err) + return fmt.Errorf("failed to open AF_SYSTEM: %w", err) } var ar in6_aliasreq @@ -117,16 +147,16 @@ func (tun *TunAdapter) setupAddress(addr string) error { tun.log.Infof("Interface IPv6: %s", addr) tun.log.Infof("Interface MTU: %d", ir.ifru_mtu) - if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(darwin_SIOCAIFADDR_IN6), uintptr(unsafe.Pointer(&ar))); errno != 0 { + if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(darwin_SIOCAIFADDR_IN6), uintptr(unsafe.Pointer(&ar))); errno != 0 { // nolint:staticcheck err = errno tun.log.Errorf("Error in darwin_SIOCAIFADDR_IN6: %v", errno) - return err + return fmt.Errorf("failed to call SIOCAIFADDR_IN6: %w", err) } - if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(unix.SIOCSIFMTU), uintptr(unsafe.Pointer(&ir))); errno != 0 { + if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(unix.SIOCSIFMTU), uintptr(unsafe.Pointer(&ir))); errno != 0 { // nolint:staticcheck err = errno tun.log.Errorf("Error in SIOCSIFMTU: %v", errno) - return err + return fmt.Errorf("failed to call SIOCSIFMTU: %w", err) } return err diff --git a/src/tuntap/tun_bsd.go b/src/tun/tun_freebsd.go similarity index 79% rename from src/tuntap/tun_bsd.go rename to src/tun/tun_freebsd.go index fe36266b..7b8ab50c 100644 --- a/src/tuntap/tun_bsd.go +++ b/src/tun/tun_freebsd.go @@ -1,10 +1,11 @@ -//go:build openbsd || freebsd -// +build openbsd freebsd +//go:build freebsd +// +build freebsd -package tuntap +package tun import ( "encoding/binary" + "fmt" "os/exec" "strconv" "strings" @@ -53,11 +54,6 @@ struct in6_ifreq { 290 }; */ -type in6_ifreq_mtu struct { - ifr_name [syscall.IFNAMSIZ]byte - ifru_mtu int -} - type in6_ifreq_addr struct { ifr_name [syscall.IFNAMSIZ]byte ifru_addr sockaddr_in6 @@ -77,7 +73,7 @@ type in6_ifreq_lifetime struct { func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error { iface, err := wgtun.CreateTUN(ifname, int(mtu)) if err != nil { - panic(err) + return fmt.Errorf("failed to create TUN: %w", err) } tun.iface = iface if mtu, err := iface.MTU(); err == nil { @@ -85,7 +81,15 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error { } else { tun.mtu = 0 } - return tun.setupAddress(addr) + if addr != "" { + return tun.setupAddress(addr) + } + return nil +} + +// Configures the "utun" adapter from an existing file descriptor. +func (tun *TunAdapter) setupFD(fd int32, addr string, mtu uint64) error { + return fmt.Errorf("setup via FD not supported on this platform") } func (tun *TunAdapter) setupAddress(addr string) error { @@ -103,26 +107,6 @@ func (tun *TunAdapter) setupAddress(addr string) error { tun.log.Infof("Interface IPv6: %s", addr) tun.log.Infof("Interface MTU: %d", tun.mtu) - // Create the MTU request - var ir in6_ifreq_mtu - copy(ir.ifr_name[:], tun.Name()) - ir.ifru_mtu = int(tun.mtu) - - // Set the MTU - if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(sfd), uintptr(syscall.SIOCSIFMTU), uintptr(unsafe.Pointer(&ir))); errno != 0 { - err = errno - tun.log.Errorf("Error in SIOCSIFMTU: %v", errno) - - // Fall back to ifconfig to set the MTU - cmd := exec.Command("ifconfig", tun.Name(), "mtu", string(tun.mtu)) - tun.log.Warnf("Using ifconfig as fallback: %v", strings.Join(cmd.Args, " ")) - output, err := cmd.CombinedOutput() - if err != nil { - tun.log.Errorf("SIOCSIFMTU fallback failed: %v.", err) - tun.log.Traceln(string(output)) - } - } - // Create the address request // FIXME: I don't work! var ar in6_ifreq_addr diff --git a/src/tuntap/tun_linux.go b/src/tun/tun_linux.go similarity index 62% rename from src/tuntap/tun_linux.go rename to src/tun/tun_linux.go index f849c00f..98d63db4 100644 --- a/src/tuntap/tun_linux.go +++ b/src/tun/tun_linux.go @@ -1,11 +1,13 @@ -//go:build !mobile -// +build !mobile +//go:build linux || android +// +build linux android -package tuntap +package tun // The linux platform specific tun parts import ( + "fmt" + "github.com/vishvananda/netlink" wgtun "golang.zx2c4.com/wireguard/tun" ) @@ -17,7 +19,7 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error { } iface, err := wgtun.CreateTUN(ifname, int(mtu)) if err != nil { - panic(err) + return fmt.Errorf("failed to create TUN: %w", err) } tun.iface = iface if mtu, err := iface.MTU(); err == nil { @@ -25,30 +27,38 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error { } else { tun.mtu = 0 } - return tun.setupAddress(addr) + if addr != "" { + return tun.setupAddress(addr) + } + return nil } -// Configures the TAP adapter with the correct IPv6 address and MTU. Netlink +// Configures the "utun" adapter from an existing file descriptor. +func (tun *TunAdapter) setupFD(fd int32, addr string, mtu uint64) error { + return fmt.Errorf("setup via FD not supported on this platform") +} + +// Configures the TUN adapter with the correct IPv6 address and MTU. Netlink // is used to do this, so there is not a hard requirement on "ip" or "ifconfig" // to exist on the system, but this will fail if Netlink is not present in the // kernel (it nearly always is). func (tun *TunAdapter) setupAddress(addr string) error { nladdr, err := netlink.ParseAddr(addr) if err != nil { - return err + return fmt.Errorf("couldn't parse address %q: %w", addr, err) } nlintf, err := netlink.LinkByName(tun.Name()) if err != nil { - return err + return fmt.Errorf("failed to find link by name: %w", err) } if err := netlink.AddrAdd(nlintf, nladdr); err != nil { - return err + return fmt.Errorf("failed to add address to link: %w", err) } if err := netlink.LinkSetMTU(nlintf, int(tun.mtu)); err != nil { - return err + return fmt.Errorf("failed to set link MTU: %w", err) } if err := netlink.LinkSetUp(nlintf); err != nil { - return err + return fmt.Errorf("failed to bring link up: %w", err) } // Friendly output tun.log.Infof("Interface name: %s", tun.Name()) diff --git a/src/tun/tun_openbsd.go b/src/tun/tun_openbsd.go new file mode 100644 index 00000000..714db3a8 --- /dev/null +++ b/src/tun/tun_openbsd.go @@ -0,0 +1,122 @@ +//go:build openbsd +// +build openbsd + +package tun + +import ( + "fmt" + "net" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" + + wgtun "golang.zx2c4.com/wireguard/tun" +) + +const ( + SIOCAIFADDR_IN6 = 0x8080691a + ND6_INFINITE_LIFETIME = 0xffffffff +) + +type in6_addrlifetime struct { + ia6t_expire int64 + ia6t_preferred int64 + ia6t_vltime uint32 + ia6t_pltime uint32 +} + +// Match types from the net package, effectively being [16]byte for IPv6 addresses. +type in6_addr [16]uint8 + +type sockaddr_in6 struct { + sin6_len uint8 + sin6_family uint8 + sin6_port uint16 + sin6_flowinfo uint32 + sin6_addr in6_addr + sin6_scope_id uint32 +} + +func (sa6 *sockaddr_in6) setSockaddr(addr [/*16*/]byte /* net.IP or net.IPMask */) { + sa6.sin6_len = uint8(unsafe.Sizeof(*sa6)) + sa6.sin6_family = unix.AF_INET6 + + for i := range sa6.sin6_addr { + sa6.sin6_addr[i] = addr[i] + } +} + +type in6_aliasreq struct { + ifra_name [syscall.IFNAMSIZ]byte + ifra_addr sockaddr_in6 + ifra_dstaddr sockaddr_in6 + ifra_prefixmask sockaddr_in6 + ifra_flags int32 + ifra_lifetime in6_addrlifetime +} + +// Configures the TUN adapter with the correct IPv6 address and MTU. +func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error { + iface, err := wgtun.CreateTUN(ifname, int(mtu)) + if err != nil { + return fmt.Errorf("failed to create TUN: %w", err) + } + tun.iface = iface + if mtu, err := iface.MTU(); err == nil { + tun.mtu = getSupportedMTU(uint64(mtu)) + } else { + tun.mtu = 0 + } + if addr != "" { + return tun.setupAddress(addr) + } + return nil +} + +// Configures the "utun" adapter from an existing file descriptor. +func (tun *TunAdapter) setupFD(fd int32, addr string, mtu uint64) error { + return fmt.Errorf("setup via FD not supported on this platform") +} + +func (tun *TunAdapter) setupAddress(addr string) error { + var sfd int + var err error + + ip, prefix, err := net.ParseCIDR(addr) + if err != nil { + tun.log.Errorf("Error in ParseCIDR: %v", err) + return err + } + + // Create system socket + if sfd, err = unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, 0); err != nil { + tun.log.Printf("Create AF_INET6 socket failed: %v", err) + return err + } + + // Friendly output + tun.log.Infof("Interface name: %s", tun.Name()) + tun.log.Infof("Interface IPv6: %s", addr) + tun.log.Infof("Interface MTU: %d", tun.mtu) + + // Create the address request + var ar in6_aliasreq + copy(ar.ifra_name[:], tun.Name()) + + ar.ifra_addr.setSockaddr(ip) + + prefixmask := net.CIDRMask(prefix.Mask.Size()) + ar.ifra_prefixmask.setSockaddr(prefixmask) + + ar.ifra_lifetime.ia6t_vltime = ND6_INFINITE_LIFETIME + ar.ifra_lifetime.ia6t_pltime = ND6_INFINITE_LIFETIME + + // Set the interface address + if err = unix.IoctlSetInt(sfd, SIOCAIFADDR_IN6, int(uintptr(unsafe.Pointer(&ar)))); err != nil { + tun.log.Errorf("Error in SIOCAIFADDR_IN6: %v", err) + return err + } + + return nil +} diff --git a/src/tuntap/tun_other.go b/src/tun/tun_other.go similarity index 61% rename from src/tuntap/tun_other.go rename to src/tun/tun_other.go index 8ce24953..0ddd0c1e 100644 --- a/src/tuntap/tun_other.go +++ b/src/tun/tun_other.go @@ -1,12 +1,14 @@ -//go:build !linux && !darwin && !windows && !openbsd && !freebsd && !mobile -// +build !linux,!darwin,!windows,!openbsd,!freebsd,!mobile +//go:build !linux && !darwin && !ios && !android && !windows && !openbsd && !freebsd && !mobile +// +build !linux,!darwin,!ios,!android,!windows,!openbsd,!freebsd,!mobile -package tuntap +package tun // This is to catch unsupported platforms // If your platform supports tun devices, you could try configuring it manually import ( + "fmt" + wgtun "golang.zx2c4.com/wireguard/tun" ) @@ -14,7 +16,7 @@ import ( func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error { iface, err := wgtun.CreateTUN(ifname, mtu) if err != nil { - panic(err) + return fmt.Errorf("failed to create TUN: %w", err) } tun.iface = iface if mtu, err := iface.MTU(); err == nil { @@ -22,7 +24,15 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error { } else { tun.mtu = 0 } - return tun.setupAddress(addr) + if addr != "" { + return tun.setupAddress(addr) + } + return nil +} + +// Configures the "utun" adapter from an existing file descriptor. +func (tun *TunAdapter) setupFD(fd int32, addr string, mtu uint64) error { + return fmt.Errorf("setup via FD not supported on this platform") } // We don't know how to set the IPv6 address on an unknown platform, therefore diff --git a/src/tuntap/tun_windows.go b/src/tun/tun_windows.go similarity index 63% rename from src/tuntap/tun_windows.go rename to src/tun/tun_windows.go index 8dce7274..a2861894 100644 --- a/src/tuntap/tun_windows.go +++ b/src/tun/tun_windows.go @@ -1,17 +1,19 @@ //go:build windows // +build windows -package tuntap +package tun import ( - "bytes" "errors" + "fmt" "log" - "net" + "net/netip" + "time" - "github.com/yggdrasil-network/yggdrasil-go/src/defaults" + "github.com/yggdrasil-network/yggdrasil-go/src/config" "golang.org/x/sys/windows" + "golang.zx2c4.com/wintun" wgtun "golang.zx2c4.com/wireguard/tun" "golang.zx2c4.com/wireguard/windows/elevate" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" @@ -22,7 +24,7 @@ import ( // Configures the TUN adapter with the correct IPv6 address and MTU. func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error { if ifname == "auto" { - ifname = defaults.GetDefaults().DefaultIfName + ifname = config.GetDefaults().DefaultIfName } return elevate.DoAsSystem(func() error { var err error @@ -31,13 +33,27 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error { if guid, err = windows.GUIDFromString("{8f59971a-7872-4aa6-b2eb-061fc4e9d0a7}"); err != nil { return err } - if iface, err = wgtun.CreateTUNWithRequestedGUID(ifname, &guid, int(mtu)); err != nil { - return err + iface, err = wgtun.CreateTUNWithRequestedGUID(ifname, &guid, int(mtu)) + if err != nil { + // Very rare condition, it will purge the old device and create new + tun.log.Printf("Error creating TUN: '%s'", err) + wintun.Uninstall() + time.Sleep(3 * time.Second) + tun.log.Printf("Trying again") + iface, err = wgtun.CreateTUNWithRequestedGUID(ifname, &guid, int(mtu)) + if err != nil { + return err + } } + tun.log.Printf("Waiting for TUN to come up") + time.Sleep(1 * time.Second) tun.iface = iface - if err = tun.setupAddress(addr); err != nil { - tun.log.Errorln("Failed to set up TUN address:", err) - return err + if addr != "" { + tun.log.Printf("Setting up address") + if err = tun.setupAddress(addr); err != nil { + tun.log.Errorln("Failed to set up TUN address:", err) + return err + } } if err = tun.setupMTU(getSupportedMTU(mtu)); err != nil { tun.log.Errorln("Failed to set up TUN MTU:", err) @@ -46,11 +62,17 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error { if mtu, err := iface.MTU(); err == nil { tun.mtu = uint64(mtu) } + tun.log.Printf("TUN is set up successfully") return nil }) } -// Sets the MTU of the TAP adapter. +// Configures the "utun" adapter from an existing file descriptor. +func (tun *TunAdapter) setupFD(fd int32, addr string, mtu uint64) error { + return fmt.Errorf("setup via FD not supported on this platform") +} + +// Sets the MTU of the TUN adapter. func (tun *TunAdapter) setupMTU(mtu uint64) error { if tun.iface == nil || tun.Name() == "" { return errors.New("Can't configure MTU as TUN adapter is not present") @@ -77,19 +99,15 @@ func (tun *TunAdapter) setupMTU(mtu uint64) error { return nil } -// Sets the IPv6 address of the TAP adapter. +// Sets the IPv6 address of the TUN adapter. func (tun *TunAdapter) setupAddress(addr string) error { if tun.iface == nil || tun.Name() == "" { return errors.New("Can't configure IPv6 address as TUN adapter is not present") } if intf, ok := tun.iface.(*wgtun.NativeTun); ok { - if ipaddr, ipnet, err := net.ParseCIDR(addr); err == nil { + if ipnet, err := netip.ParsePrefix(addr); err == nil { luid := winipcfg.LUID(intf.LUID()) - addresses := append([]net.IPNet{}, net.IPNet{ - IP: ipaddr, - Mask: ipnet.Mask, - }) - + addresses := []netip.Prefix{ipnet} err := luid.SetIPAddressesForFamily(windows.AF_INET6, addresses) if err == windows.ERROR_OBJECT_ALREADY_EXISTS { cleanupAddressesOnDisconnectedInterfaces(windows.AF_INET6, addresses) @@ -112,24 +130,13 @@ func (tun *TunAdapter) setupAddress(addr string) error { * SPDX-License-Identifier: MIT * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. */ -func cleanupAddressesOnDisconnectedInterfaces(family winipcfg.AddressFamily, addresses []net.IPNet) { +func cleanupAddressesOnDisconnectedInterfaces(family winipcfg.AddressFamily, addresses []netip.Prefix) { if len(addresses) == 0 { return } - includedInAddresses := func(a net.IPNet) bool { - // TODO: this makes the whole algorithm O(n^2). But we can't stick net.IPNet in a Go hashmap. Bummer! - for _, addr := range addresses { - ip := addr.IP - if ip4 := ip.To4(); ip4 != nil { - ip = ip4 - } - mA, _ := addr.Mask.Size() - mB, _ := a.Mask.Size() - if bytes.Equal(ip, a.IP) && mA == mB { - return true - } - } - return false + addrHash := make(map[netip.Addr]bool, len(addresses)) + for i := range addresses { + addrHash[addresses[i].Addr()] = true } interfaces, err := winipcfg.GetAdaptersAddresses(family, winipcfg.GAAFlagDefault) if err != nil { @@ -140,11 +147,10 @@ func cleanupAddressesOnDisconnectedInterfaces(family winipcfg.AddressFamily, add continue } for address := iface.FirstUnicastAddress; address != nil; address = address.Next { - ip := address.Address.IP() - ipnet := net.IPNet{IP: ip, Mask: net.CIDRMask(int(address.OnLinkPrefixLength), 8*len(ip))} - if includedInAddresses(ipnet) { - log.Printf("Cleaning up stale address %s from interface ‘%s’", ipnet.String(), iface.FriendlyName()) - iface.LUID.DeleteIPAddress(ipnet) + if ip, _ := netip.AddrFromSlice(address.Address.IP()); addrHash[ip] { + prefix := netip.PrefixFrom(ip, int(address.OnLinkPrefixLength)) + log.Printf("Cleaning up stale address %s from interface ‘%s’", prefix.String(), iface.FriendlyName()) + iface.LUID.DeleteIPAddress(prefix) } } } diff --git a/src/tuntap/admin.go b/src/tuntap/admin.go deleted file mode 100644 index 862a3c66..00000000 --- a/src/tuntap/admin.go +++ /dev/null @@ -1,37 +0,0 @@ -package tuntap - -import ( - "encoding/json" - - "github.com/yggdrasil-network/yggdrasil-go/src/admin" -) - -type GetTUNRequest struct{} -type GetTUNResponse map[string]TUNEntry - -type TUNEntry struct { - MTU uint64 `json:"mtu"` -} - -func (t *TunAdapter) getTUNHandler(req *GetTUNRequest, res *GetTUNResponse) error { - *res = GetTUNResponse{ - t.Name(): TUNEntry{ - MTU: t.MTU(), - }, - } - return nil -} - -func (t *TunAdapter) SetupAdminHandlers(a *admin.AdminSocket) { - _ = a.AddHandler("getTunTap", []string{}, func(in json.RawMessage) (interface{}, error) { - req := &GetTUNRequest{} - res := &GetTUNResponse{} - if err := json.Unmarshal(in, &req); err != nil { - return nil, err - } - if err := t.getTUNHandler(req, res); err != nil { - return nil, err - } - return res, nil - }) -} diff --git a/src/tuntap/iface.go b/src/tuntap/iface.go deleted file mode 100644 index f629399a..00000000 --- a/src/tuntap/iface.go +++ /dev/null @@ -1,47 +0,0 @@ -package tuntap - -const TUN_OFFSET_BYTES = 4 - -func (tun *TunAdapter) read() { - var buf [TUN_OFFSET_BYTES + 65535]byte - for { - n, err := tun.iface.Read(buf[:], TUN_OFFSET_BYTES) - if n <= TUN_OFFSET_BYTES || err != nil { - tun.log.Errorln("Error reading TUN:", err) - ferr := tun.iface.Flush() - if ferr != nil { - tun.log.Errorln("Unable to flush packets:", ferr) - } - return - } - begin := TUN_OFFSET_BYTES - end := begin + n - bs := buf[begin:end] - if _, err := tun.rwc.Write(bs); err != nil { - tun.log.Debugln("Unable to send packet:", err) - } - } -} - -func (tun *TunAdapter) write() { - var buf [TUN_OFFSET_BYTES + 65535]byte - for { - bs := buf[TUN_OFFSET_BYTES:] - n, err := tun.rwc.Read(bs) - if err != nil { - tun.log.Errorln("Exiting tun writer due to core read error:", err) - return - } - if !tun.isEnabled { - continue // Nothing to do, the tun isn't enabled - } - bs = buf[:TUN_OFFSET_BYTES+n] - if _, err = tun.iface.Write(bs, TUN_OFFSET_BYTES); err != nil { - tun.Act(nil, func() { - if !tun.isOpen { - tun.log.Errorln("TUN iface write error:", err) - } - }) - } - } -} diff --git a/src/util/util.go b/src/util/util.go deleted file mode 100644 index 507426d0..00000000 --- a/src/util/util.go +++ /dev/null @@ -1,37 +0,0 @@ -// Package util contains miscellaneous utilities used by yggdrasil. -// In particular, this includes a crypto worker pool, Cancellation machinery, and a sync.Pool used to reuse []byte. -package util - -// These are misc. utility functions that didn't really fit anywhere else - -import ( - "time" -) - -// TimerStop stops a timer and makes sure the channel is drained, returns true if the timer was stopped before firing. -func TimerStop(t *time.Timer) bool { - stopped := t.Stop() - select { - case <-t.C: - default: - } - return stopped -} - -// FuncTimeout runs the provided function in a separate goroutine, and returns true if the function finishes executing before the timeout passes, or false if the timeout passes. -// It includes no mechanism to stop the function if the timeout fires, so the user is expected to do so on their own (such as with a Cancellation or a context). -func FuncTimeout(timeout time.Duration, f func()) bool { - success := make(chan struct{}) - go func() { - defer close(success) - f() - }() - timer := time.NewTimer(timeout) - defer TimerStop(timer) - select { - case <-success: - return true - case <-timer.C: - return false - } -}