diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c99ac06..92ad2561 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/setup-go@v3 with: - go-version: 1.19 + go-version: 1.21 - uses: actions/checkout@v3 - name: golangci-lint uses: golangci/golangci-lint-action@v3 @@ -51,7 +51,7 @@ jobs: strategy: fail-fast: false matrix: - goversion: ["1.17", "1.18", "1.19"] + goversion: ["1.20", "1.21"] name: Build & Test (Linux, Go ${{ matrix.goversion }}) needs: [lint] @@ -75,7 +75,7 @@ jobs: strategy: fail-fast: false matrix: - goversion: ["1.17", "1.18", "1.19"] + goversion: ["1.20", "1.21"] name: Build & Test (Windows, Go ${{ matrix.goversion }}) needs: [lint] @@ -99,7 +99,7 @@ jobs: strategy: fail-fast: false matrix: - goversion: ["1.17", "1.18", "1.19"] + goversion: ["1.20", "1.21"] name: Build & Test (macOS, Go ${{ matrix.goversion }}) needs: [lint] @@ -119,6 +119,32 @@ jobs: - name: Unit tests run: go test -v ./... + build-freebsd: + strategy: + fail-fast: false + matrix: + goversion: ["1.20", "1.21"] + goos: + - freebsd + - openbsd + + name: Build (Cross ${{ matrix.goos }}, Go ${{ matrix.goversion }}) + needs: [lint] + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + 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] @@ -128,4 +154,4 @@ jobs: - name: Check all tests passed uses: re-actors/alls-green@release/v1 with: - jobs: ${{ toJSON(needs) }} \ No newline at end of file + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/pkg.yml b/.github/workflows/pkg.yml index f282cc21..f47baa0f 100644 --- a/.github/workflows/pkg.yml +++ b/.github/workflows/pkg.yml @@ -16,7 +16,7 @@ jobs: name: Package (Debian, ${{ matrix.pkgarch }}) - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 with: @@ -25,7 +25,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.19 + go-version: "1.20" - name: Build package env: @@ -56,7 +56,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.19 + go-version: "1.20" - name: Build package env: @@ -87,7 +87,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.19 + go-version: "1.20" - name: Build package run: sh contrib/msi/build-msi.sh ${{ matrix.pkgarch }} @@ -107,7 +107,7 @@ jobs: name: Package (Router, ${{ matrix.pkgarch }}) - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 with: @@ -122,7 +122,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.19 + go-version: "1.20" - name: Build package env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a43a4d1..50296af0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,708 +26,741 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - in case of vulnerabilities. --> +## [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 +* 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 +* 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 +* 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 +* 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 +* 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 +* 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 +* 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 +* `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 +* 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.17 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/build b/build index c7214438..de5d9edc 100755 --- a/build +++ b/build @@ -6,7 +6,7 @@ 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)} -LDFLAGS="-X $PKGSRC.buildName=$PKGNAME -X $PKGSRC.buildVersion=$PKGVER" +LDFLAGS="${LDFLAGS} -X $PKGSRC.buildName=$PKGNAME -X $PKGSRC.buildVersion=$PKGVER" ARGS="-v" while getopts "utc:l:dro:p" option diff --git a/cmd/genkeys/main.go b/cmd/genkeys/main.go index 81942446..36107c0a 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,7 @@ import ( "fmt" "net" "runtime" + "time" "github.com/yggdrasil-network/yggdrasil-go/src/address" ) @@ -29,6 +28,8 @@ type keySet struct { func main() { threads := runtime.GOMAXPROCS(0) + fmt.Println("Threads:", threads) + start := time.Now() var currentBest ed25519.PublicKey newKeys := make(chan keySet, threads) for i := 0; i < threads; i++ { @@ -38,7 +39,7 @@ func main() { newKey := <-newKeys if isBetter(currentBest, newKey.pub) || len(currentBest) == 0 { currentBest = newKey.pub - fmt.Println("-----") + fmt.Println("-----", time.Since(start)) fmt.Println("Priv:", hex.EncodeToString(newKey.priv)) fmt.Println("Pub:", hex.EncodeToString(newKey.pub)) addr := address.AddrForKey(newKey.pub) diff --git a/cmd/yggdrasil/main.go b/cmd/yggdrasil/main.go index f85525d6..e0b33990 100644 --- a/cmd/yggdrasil/main.go +++ b/cmd/yggdrasil/main.go @@ -1,34 +1,27 @@ package main import ( - "bytes" "context" "crypto/ed25519" "encoding/hex" "encoding/json" "flag" "fmt" - "io" "net" "os" "os/signal" "regexp" "strings" - "sync" "syscall" - "golang.org/x/text/encoding/unicode" - "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" @@ -44,75 +37,240 @@ type node struct { 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 = os.ReadFile(useconffile) +// The main function is responsible for configuring and starting Yggdrasil. +func main() { + 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, "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() + + // Catch interrupts from the operating system to exit gracefully. + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + + // Capture the service being stopped on Windows. + minwinsvc.SetOnExit(cancel) + + // 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 = io.ReadAll(os.Stdin) + setLogLevel(*loglevel, logger) } - if err != nil { - panic(err) - } - // 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) + + 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) } - } - // 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) - } - // 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 -} + if _, err := cfg.ReadFrom(f); err != nil { + panic(err) + } + _ = f.Close() -// 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) + 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 err != nil { - panic(err) + + 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 *normaliseconf: + 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 } - return string(bs) + + n := &node{} + + // Setup the Yggdrasil node itself. + { + options := []core.SetupOption{ + core.NodeInfo(cfg.NodeInfo), + core.NodeInfoPrivacy(cfg.NodeInfoPrivacy), + } + 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.Infof("Your public key is %s", hex.EncodeToString(n.core.PublicKey())) + logger.Infof("Your IPv6 address is %s", address.String()) + logger.Infof("Your IPv6 subnet is %s", subnet.String()) + } + + // Setup 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() + } + } + + // Setup 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) + } + } + + // Setup 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) + } + } + + // 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) { @@ -140,269 +298,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) { - // 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 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. - fmt.Println("Usage:") - flag.PrintDefaults() - - if args.getaddr || args.getsnet { - fmt.Println("\nError: You need to specify some config data using -useconf or -useconffile.") - } - } - // 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 - } - - n := &node{} - - // Setup the Yggdrasil node itself. - { - sk, err := hex.DecodeString(cfg.PrivateKey) - if err != nil { - panic(err) - } - options := []core.SetupOption{ - core.NodeInfo(cfg.NodeInfo), - core.NodeInfoPrivacy(cfg.NodeInfoPrivacy), - } - 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(sk[:], logger, options...); err != nil { - panic(err) - } - } - - // Setup the admin socket. - { - options := []admin.SetupOption{ - admin.ListenAddress(cfg.AdminListen), - } - if n.admin, err = admin.New(n.core, logger, options...); err != nil { - panic(err) - } - if n.admin != nil { - n.admin.SetupAdminHandlers() - } - } - - // Setup 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), - }) - } - 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) - } - } - - // Setup 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) - } - } - - // 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()) - - // 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 main() { - args := getArgs() - - // Catch interrupts from the operating system to exit gracefully. - ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) - - // Capture the service being stopped on Windows. - minwinsvc.SetOnExit(cancel) - - // Start the node, block and then wait for it to shut down. - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - run(args, ctx) - }() - wg.Wait() -} diff --git a/cmd/yggdrasilctl/cmd_line_env.go b/cmd/yggdrasilctl/cmd_line_env.go index 9fcabad9..b350b7e7 100644 --- a/cmd/yggdrasilctl/cmd_line_env.go +++ b/cmd/yggdrasilctl/cmd_line_env.go @@ -7,10 +7,10 @@ import ( "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 { @@ -21,7 +21,7 @@ type CmdLineEnv struct { func newCmdLineEnv() CmdLineEnv { var cmdLineEnv CmdLineEnv - cmdLineEnv.endpoint = defaults.GetDefaults().DefaultAdminListen + cmdLineEnv.endpoint = config.GetDefaults().DefaultAdminListen return cmdLineEnv } @@ -38,10 +38,9 @@ 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") @@ -58,31 +57,31 @@ func (cmdLineEnv *CmdLineEnv) parseFlagsAndArgs() { func (cmdLineEnv *CmdLineEnv) setEndpoint(logger *log.Logger) { if cmdLineEnv.server == cmdLineEnv.endpoint { - if config, err := os.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 c9b1522a..8f99e0d2 100644 --- a/cmd/yggdrasilctl/main.go +++ b/cmd/yggdrasilctl/main.go @@ -165,7 +165,7 @@ func run() int { table.Append([]string{"Build version:", resp.BuildVersion}) table.Append([]string{"IPv6 address:", resp.IPAddress}) table.Append([]string{"IPv6 subnet:", resp.Subnet}) - table.Append([]string{"Coordinates:", fmt.Sprintf("%v", resp.Coords)}) + table.Append([]string{"Routing table size:", fmt.Sprintf("%d", resp.RoutingEntries)}) table.Append([]string{"Public key:", resp.PublicKey}) table.Render() @@ -174,33 +174,49 @@ func run() int { if err := json.Unmarshal(recv.Response, &resp); err != nil { panic(err) } - table.SetHeader([]string{"Port", "Public Key", "IP Address", "Uptime", "RX", "TX", "Pr", "URI"}) + table.SetHeader([]string{"URI", "State", "Dir", "IP Address", "Uptime", "RX", "TX", "Pr", "Last Error"}) for _, peer := range resp.Peers { + state, lasterr, dir := "Up", "-", "Out" + if !peer.Up { + state, lasterr = "Down", fmt.Sprintf("%s ago: %s", peer.LastErrorTime.Round(time.Second), peer.LastError) + } + if peer.Inbound { + dir = "In" + } + uri, err := url.Parse(peer.URI) + if err != nil { + panic(err) + } + uri.RawQuery = "" table.Append([]string{ - fmt.Sprintf("%d", peer.Port), - peer.PublicKey, + uri.String(), + state, + dir, peer.IPAddress, (time.Duration(peer.Uptime) * time.Second).String(), peer.RXBytes.String(), peer.TXBytes.String(), fmt.Sprintf("%d", peer.Priority), - peer.Remote, + lasterr, }) } table.Render() - case "getdht": - var resp admin.GetDHTResponse + 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"}) - for _, dht := range resp.DHT { + //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{ - dht.PublicKey, - dht.IPAddress, - fmt.Sprintf("%d", dht.Port), - fmt.Sprintf("%d", dht.Rest), + tree.PublicKey, + tree.IPAddress, + tree.Parent, + fmt.Sprintf("%d", tree.Sequence), + //fmt.Sprintf("%d", dht.Port), + //fmt.Sprintf("%d", dht.Rest), }) } table.Render() @@ -210,12 +226,13 @@ func run() int { if err := json.Unmarshal(recv.Response, &resp); err != nil { panic(err) } - table.SetHeader([]string{"Public Key", "IP Address", "Path"}) + 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() 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/deb/generate.sh b/contrib/deb/generate.sh index ebe2753a..5b4275a9 100644 --- a/contrib/deb/generate.sh +++ b/contrib/deb/generate.sh @@ -21,6 +21,9 @@ if [ $PKGBRANCH = "master" ]; then PKGREPLACES=yggdrasil-develop fi +export LDFLAGS="-X github.com/yggdrasil-network/yggdrasil-go/src/config.defaultConfig=/etc/yggdrasil/yggdrasil.conf" +export LDFLAGS="${LDFLAGS} -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 elif [ $PKGARCH = "i386" ]; then GOARCH=386 GOOS=linux ./build elif [ $PKGARCH = "mipsel" ]; then GOARCH=mipsle GOOS=linux ./build @@ -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/usr/lib/systemd/system/ cat > /tmp/$PKGNAME/debian/changelog << EOF Please see https://github.com/yggdrasil-network/yggdrasil-go/ @@ -68,35 +71,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 +usr/lib/systemd/system/*.service usr/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 +130,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/usr/lib/systemd/system/yggdrasil-default-config.service +cp contrib/systemd/yggdrasil.service.debian /tmp/$PKGNAME/usr/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 . + usr/lib/systemd/system/yggdrasil.service \ + usr/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..3f6b9bfc 100755 --- a/contrib/mobile/build +++ b/contrib/mobile/build @@ -37,7 +37,7 @@ 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 3b3227bd..cf026a70 100644 --- a/contrib/mobile/mobile.go +++ b/contrib/mobile/mobile.go @@ -3,7 +3,6 @@ package mobile import ( "encoding/hex" "encoding/json" - "fmt" "net" "regexp" @@ -12,9 +11,9 @@ import ( "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" @@ -30,7 +29,9 @@ type Yggdrasil struct { iprwc *ipv6rwc.ReadWriteCloser config *config.NodeConfig multicast *multicast.Multicast + tun *tun.TunAdapter // optional log MobileLogger + logger *log.Logger } // StartAutoconfigure starts a node with a randomly generated config @@ -41,20 +42,19 @@ 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 } // Setup the Yggdrasil node itself. { - sk, err := hex.DecodeString(m.config.PrivateKey) - if err != nil { - panic(err) - } options := []core.SetupOption{} for _, peer := range m.config.Peers { options = append(options, core.Peer{URI: peer}) @@ -71,15 +71,24 @@ func (m *Yggdrasil) StartJSON(configjson []byte) error { } options = append(options, core.AllowedPublicKey(k[:])) } - m.core, err = core.New(sk[:], logger, options...) + 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()) } // Setup 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{ @@ -88,9 +97,11 @@ func (m *Yggdrasil) StartJSON(configjson []byte) error { Listen: intf.Listen, Port: intf.Port, Priority: uint8(intf.Priority), + Password: intf.Password, }) } - m.multicast, err = multicast.New(m.core, logger, options...) + 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) } @@ -151,10 +162,20 @@ func (m *Yggdrasil) RecvBuffer(buf []byte) (int, error) { 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 } @@ -166,7 +187,7 @@ func (m *Yggdrasil) 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 @@ -191,9 +212,9 @@ 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) { @@ -202,8 +223,11 @@ func (m *Yggdrasil) GetPeersJSON() (result string) { 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.PeerInfo IP string @@ -219,8 +243,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 "{}" diff --git a/contrib/mobile/mobile_ios.go b/contrib/mobile/mobile_ios.go index fedee2db..c7747ea3 100644 --- a/contrib/mobile/mobile_ios.go +++ b/contrib/mobile/mobile_ios.go @@ -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_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/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 7d6b3bd5..faa0e0d8 100644 --- a/go.mod +++ b/go.mod @@ -1,38 +1,47 @@ module github.com/yggdrasil-network/yggdrasil-go -go 1.17 +go 1.20 require ( - github.com/Arceliar/ironwood v0.0.0-20221115123222-ec61cea2f439 - 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-20231028101932-ceac99571f43 + github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d + github.com/cheggaaa/pb/v3 v3.1.4 + 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/hjson/hjson-go/v4 v4.3.0 github.com/kardianos/minwinsvc v1.0.2 - github.com/mitchellh/mapstructure v1.4.1 + github.com/quic-go/quic-go v0.39.3 github.com/vishvananda/netlink v1.1.0 - golang.org/x/mobile v0.0.0-20221110043201-43a038452099 - golang.org/x/net v0.0.0-20221014081412-f15817d10f9b - golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43 - golang.org/x/text v0.3.8 - golang.zx2c4.com/wireguard v0.0.0-20211017052713-f87e87af0d9a - golang.zx2c4.com/wireguard/windows v0.4.12 + golang.org/x/crypto v0.14.0 + golang.org/x/mobile v0.0.0-20231006135142-2b44d11868fe + golang.org/x/net v0.17.0 + golang.org/x/sys v0.13.0 + golang.org/x/text v0.13.0 + golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675 + golang.zx2c4.com/wireguard/windows v0.5.3 ) require ( - github.com/mattn/go-colorable v0.1.8 // indirect + github.com/bits-and-blooms/bitset v1.5.0 // indirect + github.com/bits-and-blooms/bloom/v3 v3.3.1 // 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/quic-go/qtls-go1-20 v0.3.4 // indirect github.com/rivo/uniseg v0.2.0 // indirect - golang.org/x/crypto v0.0.0-20221012134737-56aed061732a // indirect - golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect - golang.org/x/tools v0.1.12 // indirect + go.uber.org/mock v0.3.0 // indirect + golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect + golang.org/x/mod v0.13.0 // indirect + golang.org/x/tools v0.14.0 // indirect + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect ) require ( github.com/VividCortex/ewma v1.2.0 // indirect - github.com/fatih/color v1.12.0 // indirect - github.com/mattn/go-isatty v0.0.13 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect github.com/olekukonko/tablewriter v0.0.5 github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect ) diff --git a/go.sum b/go.sum index 3c2f7310..901eabaa 100644 --- a/go.sum +++ b/go.sum @@ -1,120 +1,137 @@ -github.com/Arceliar/ironwood v0.0.0-20221115123222-ec61cea2f439 h1:eOW6/XIs06TnUn9GPCnfv71CQZw8edP3u3mH3lZt6iM= -github.com/Arceliar/ironwood v0.0.0-20221115123222-ec61cea2f439/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-20231028101932-ceac99571f43 h1:M4NczBPk7Fy0Uy2YvNoXwSkk3dGoGTOYtUjyqpOC5ko= +github.com/Arceliar/ironwood v0.0.0-20231028101932-ceac99571f43/go.mod h1:5x7fWW0mshe9WQ1lvSMmmHBYC3BeHH9gpwW5tz7cbfw= +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.3.1/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bits-and-blooms/bitset v1.5.0 h1:NpE8frKRLGHIcEzkR+gZhiioW1+WbYV6fKwD6ZIpQT8= +github.com/bits-and-blooms/bitset v1.5.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bits-and-blooms/bloom/v3 v3.3.1 h1:K2+A19bXT8gJR5mU7y+1yW6hsKfNCjcP2uNfLFKncjQ= +github.com/bits-and-blooms/bloom/v3 v3.3.1/go.mod h1:bhUUknWd5khVbTe4UgMCSiOOVJzr3tMoijSK3WwvW90= +github.com/cheggaaa/pb/v3 v3.1.4 h1:DN8j4TVVdKu3WxVwcRKu0sG00IIU6FewoABZzXbRQeo= +github.com/cheggaaa/pb/v3 v3.1.4/go.mod h1:6wVjILNBaXMs8c21qRiaUM8BR82erfgau1DQ4iUXmSA= +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/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.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +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/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/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +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/hjson/hjson-go/v4 v4.3.0 h1:dyrzJdqqFGhHt+FSrs5n9s6b0fPM8oSJdWo+oS3YnJw= +github.com/hjson/hjson-go/v4 v4.3.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/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-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.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/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.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/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +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/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/qtls-go1-20 v0.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg= +github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= +github.com/quic-go/quic-go v0.39.3 h1:o3YB6t2SR+HU/pgwF29kJ6g4jJIJEwEZ8CKia1h1TKg= +github.com/quic-go/quic-go v0.39.3/go.mod h1:T09QsDQWjLiQ74ZmacDfqZmhY/NLnw5BC40MANNNZ1Q= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.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= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= +go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= 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/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20221012134737-56aed061732a h1:NmSIgad6KjE6VvHciPZuNRTKxGhlPfD6OA87W/PLkqg= -golang.org/x/crypto v0.0.0-20221012134737-56aed061732a/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -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-20221110043201-43a038452099 h1:aIu0lKmfdgtn2uTj7JI2oN4TUrQvgB+wzTPO23bCKt8= -golang.org/x/mobile v0.0.0-20221110043201-43a038452099/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= +golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/mobile v0.0.0-20231006135142-2b44d11868fe h1:lrXv4yHeD9FA8PSJATWowP1QvexpyAPWmPia+Kbzql8= +golang.org/x/mobile v0.0.0-20231006135142-2b44d11868fe/go.mod h1:BrnXpEObnFxpaT75Jo9hsCazwOWcp7nVIa8NNuH5cuA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -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/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20221014081412-f15817d10f9b h1:tvrvnPFcdzp294diPnrdZZZ8XUt2Tyj7svb7X52iDuU= -golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 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/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= 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-20191204072324-ce4227a45e2e/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-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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43 h1:OK7RB6t2WQX54srQQYSXMW8dF5C6/8+oA/s5QBmmto4= -golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b/go.mod h1:EFNZuWvGYxIRUEX+K8UmCFwYmZjqcrnq15ZuVldZkZ0= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= 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/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= +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-20230223181233-21636207a675 h1:/J/RVnr7ng4fWPRH3xa4WtBJ1Jp+Auu4YNLmGiPv5QU= +golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675/go.mod h1:whfbyDBt09xhCYQWtO2+3UVjlaq6/9hDZrjg2ZE6SyA= +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.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +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= 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 index f5e37ee9..843ed52a 100644 --- a/src/admin/addpeer.go +++ b/src/admin/addpeer.go @@ -1,5 +1,10 @@ package admin +import ( + "fmt" + "net/url" +) + type AddPeerRequest struct { Uri string `json:"uri"` Sintf string `json:"interface,omitempty"` @@ -8,5 +13,9 @@ type AddPeerRequest struct { type AddPeerResponse struct{} func (a *AdminSocket) addPeerHandler(req *AddPeerRequest, res *AddPeerResponse) error { - return a.core.AddPeer(req.Uri, req.Sintf) + 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 9dbcfdca..8fa76c07 100644 --- a/src/admin/admin.go +++ b/src/admin/admin.go @@ -35,10 +35,10 @@ type AdminSocketRequest struct { } type AdminSocketResponse struct { - Status string `json:"status"` - Error string `json:"error,omitempty"` - Request json.RawMessage `json:"request"` - Response json.RawMessage `json:"response"` + Status string `json:"status"` + Error string `json:"error,omitempty"` + Request AdminSocketRequest `json:"request"` + Response json.RawMessage `json:"response"` } type handler struct { @@ -132,14 +132,14 @@ func (a *AdminSocket) SetupAdminHandlers() { }, ) _ = a.AddHandler( - "getDHT", "Show known DHT entries", []string{}, + "getTree", "Show known Tree entries", []string{}, func(in json.RawMessage) (interface{}, error) { - req := &GetDHTRequest{} - res := &GetDHTResponse{} + req := &GetTreeRequest{} + res := &GetTreeResponse{} if err := json.Unmarshal(in, &req); err != nil { return nil, err } - if err := a.getDHTHandler(req, res); err != nil { + if err := a.getTreeHandler(req, res); err != nil { return nil, err } return res, nil @@ -309,18 +309,22 @@ 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) + /* + defer func() { + r := recover() + if r != nil { + fmt.Println("ERROR:", r) + a.log.Debugln("Admin socket error:", r) + if err := encoder.Encode(&ErrorResponse{ + Error: "Check your syntax and input types", + }); err != nil { + fmt.Println("ERROR 2:", err) + a.log.Debugln("Admin socket JSON encode error:", err) + } + conn.Close() } - conn.Close() - } - }() + }() + */ for { var err error @@ -335,6 +339,7 @@ func (a *AdminSocket) handleRequest(conn net.Conn) { 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") } diff --git a/src/admin/getdht.go b/src/admin/getdht.go deleted file mode 100644 index bfb21818..00000000 --- a/src/admin/getdht.go +++ /dev/null @@ -1,41 +0,0 @@ -package admin - -import ( - "encoding/hex" - "net" - "sort" - "strings" - - "github.com/yggdrasil-network/yggdrasil-go/src/address" -) - -type GetDHTRequest struct{} - -type GetDHTResponse struct { - DHT []DHTEntry `json:"dht"` -} - -type DHTEntry struct { - IPAddress string `json:"address"` - PublicKey string `json:"key"` - Port uint64 `json:"port"` - Rest uint64 `json:"rest"` -} - -func (a *AdminSocket) getDHTHandler(req *GetDHTRequest, res *GetDHTResponse) error { - dht := a.core.GetDHT() - res.DHT = make([]DHTEntry, 0, len(dht)) - for _, d := range dht { - addr := address.AddrForKey(d.Key) - res.DHT = append(res.DHT, DHTEntry{ - IPAddress: net.IP(addr[:]).String(), - PublicKey: hex.EncodeToString(d.Key[:]), - Port: d.Port, - Rest: d.Rest, - }) - } - sort.SliceStable(res.DHT, func(i, j int) bool { - return strings.Compare(res.DHT[i].PublicKey, res.DHT[j].PublicKey) < 0 - }) - return nil -} diff --git a/src/admin/getpaths.go b/src/admin/getpaths.go index fbd52481..66e11bd2 100644 --- a/src/admin/getpaths.go +++ b/src/admin/getpaths.go @@ -20,6 +20,7 @@ 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 { @@ -31,6 +32,7 @@ func (a *AdminSocket) getPathsHandler(req *GetPathsRequest, res *GetPathsRespons IPAddress: net.IP(addr[:]).String(), PublicKey: hex.EncodeToString(p.Key), Path: p.Path, + Sequence: p.Sequence, }) } sort.SliceStable(res.Paths, func(i, j int) bool { diff --git a/src/admin/getpeers.go b/src/admin/getpeers.go index d51b184d..c6cd5be4 100644 --- a/src/admin/getpeers.go +++ b/src/admin/getpeers.go @@ -4,6 +4,7 @@ import ( "encoding/hex" "net" "sort" + "time" "github.com/yggdrasil-network/yggdrasil-go/src/address" ) @@ -16,33 +17,43 @@ type GetPeersResponse struct { } type PeerEntry struct { - IPAddress string `json:"address"` - PublicKey string `json:"key"` - Port uint64 `json:"port"` - Priority uint64 `json:"priority"` - Coords []uint64 `json:"coords"` - Remote string `json:"remote"` - RXBytes DataUnit `json:"bytes_recvd"` - TXBytes DataUnit `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"` + RXBytes DataUnit `json:"bytes_recvd,omitempty"` + TXBytes DataUnit `json:"bytes_sent,omitempty"` + Uptime float64 `json:"uptime,omitempty"` + LastError string `json:"last_error,omitempty"` + LastErrorTime time.Duration `json:"last_error_time,omitempty"` } func (a *AdminSocket) getPeersHandler(req *GetPeersRequest, res *GetPeersResponse) error { peers := a.core.GetPeers() res.Peers = make([]PeerEntry, 0, len(peers)) for _, p := range peers { - addr := address.AddrForKey(p.Key) - res.Peers = append(res.Peers, PeerEntry{ - IPAddress: net.IP(addr[:]).String(), - PublicKey: hex.EncodeToString(p.Key), - Port: p.Port, - Priority: uint64(p.Priority), // can't be uint8 thanks to gobind - Coords: p.Coords, - Remote: p.Remote, - RXBytes: DataUnit(p.RXBytes), - TXBytes: DataUnit(p.TXBytes), - Uptime: p.Uptime.Seconds(), - }) + peer := PeerEntry{ + Port: p.Port, + Up: p.Up, + Inbound: p.Inbound, + Priority: uint64(p.Priority), // can't be uint8 thanks to gobind + URI: p.URI, + RXBytes: DataUnit(p.RXBytes), + TXBytes: DataUnit(p.TXBytes), + Uptime: p.Uptime.Seconds(), + } + 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) } sort.Slice(res.Peers, func(i, j int) bool { if res.Peers[i].Port == res.Peers[j].Port { diff --git a/src/admin/getself.go b/src/admin/getself.go index f42dc750..9b052101 100644 --- a/src/admin/getself.go +++ b/src/admin/getself.go @@ -9,12 +9,12 @@ import ( type GetSelfRequest struct{} type GetSelfResponse struct { - BuildName string `json:"build_name"` - BuildVersion string `json:"build_version"` - PublicKey string `json:"key"` - IPAddress string `json:"address"` - Coords []uint64 `json:"coords"` - Subnet string `json:"subnet"` + 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"` } func (a *AdminSocket) getSelfHandler(req *GetSelfRequest, res *GetSelfResponse) error { @@ -25,6 +25,6 @@ func (a *AdminSocket) getSelfHandler(req *GetSelfRequest, res *GetSelfResponse) res.PublicKey = hex.EncodeToString(self.Key[:]) res.IPAddress = a.core.Address().String() res.Subnet = snet.String() - res.Coords = self.Coords + res.RoutingEntries = self.RoutingEntries return nil } diff --git a/src/admin/gettree.go b/src/admin/gettree.go new file mode 100644 index 00000000..06cf8e73 --- /dev/null +++ b/src/admin/gettree.go @@ -0,0 +1,45 @@ +package admin + +import ( + "encoding/hex" + "net" + "sort" + "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"` + //Port uint64 `json:"port"` + //Rest uint64 `json:"rest"` +} + +func (a *AdminSocket) getTreeHandler(req *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, + //Port: d.Port, + //Rest: d.Rest, + }) + } + sort.SliceStable(res.Tree, func(i, j int) bool { + return strings.Compare(res.Tree[i].PublicKey, res.Tree[j].PublicKey) < 0 + }) + return nil +} diff --git a/src/admin/options.go b/src/admin/options.go index 4607384f..03cf74ab 100644 --- a/src/admin/options.go +++ b/src/admin/options.go @@ -1,9 +1,24 @@ 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() } } @@ -14,3 +29,51 @@ type SetupOption interface { 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 index 145b8524..6b2e162e 100644 --- a/src/admin/removepeer.go +++ b/src/admin/removepeer.go @@ -1,5 +1,10 @@ package admin +import ( + "fmt" + "net/url" +) + type RemovePeerRequest struct { Uri string `json:"uri"` Sintf string `json:"interface,omitempty"` @@ -8,5 +13,9 @@ type RemovePeerRequest struct { type RemovePeerResponse struct{} func (a *AdminSocket) removePeerHandler(req *RemovePeerRequest, res *RemovePeerResponse) error { - return a.core.RemovePeer(req.Uri, req.Sintf) + 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 f7f0f6ba..e899a35d 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -17,26 +17,43 @@ configuration option that is not provided. package config import ( + "bytes" "crypto/ed25519" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" "encoding/hex" + "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 { + PrivateKey KeyBytes `comment:"Your private key. DO NOT share this with anyone!"` + PrivateKeyPath string `json:",omitempty"` + Certificate *tls.Certificate `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."` + 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. 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!"` 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."` 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."` + LogLookups bool `json:",omitempty"` } type MulticastInterfaceConfig struct { @@ -45,16 +62,199 @@ type MulticastInterfaceConfig struct { Listen bool Port uint16 Priority uint64 // 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 97% rename from src/defaults/defaults_darwin.go rename to src/config/defaults_darwin.go index 3ffd9ffb..e851d6b9 100644 --- a/src/defaults/defaults_darwin.go +++ b/src/config/defaults_darwin.go @@ -1,7 +1,7 @@ //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. diff --git a/src/defaults/defaults_freebsd.go b/src/config/defaults_freebsd.go similarity index 97% rename from src/defaults/defaults_freebsd.go rename to src/config/defaults_freebsd.go index 43359389..97f7b4c3 100644 --- a/src/defaults/defaults_freebsd.go +++ b/src/config/defaults_freebsd.go @@ -1,7 +1,7 @@ //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. diff --git a/src/defaults/defaults_linux.go b/src/config/defaults_linux.go similarity index 97% rename from src/defaults/defaults_linux.go rename to src/config/defaults_linux.go index bad6233e..6f7cbfc3 100644 --- a/src/defaults/defaults_linux.go +++ b/src/config/defaults_linux.go @@ -1,7 +1,7 @@ //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. diff --git a/src/defaults/defaults_openbsd.go b/src/config/defaults_openbsd.go similarity index 97% rename from src/defaults/defaults_openbsd.go rename to src/config/defaults_openbsd.go index e5d1fa9a..81ddf7e8 100644 --- a/src/defaults/defaults_openbsd.go +++ b/src/config/defaults_openbsd.go @@ -1,7 +1,7 @@ //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. diff --git a/src/defaults/defaults_other.go b/src/config/defaults_other.go similarity index 97% rename from src/defaults/defaults_other.go rename to src/config/defaults_other.go index bb22864e..8299364c 100644 --- a/src/defaults/defaults_other.go +++ b/src/config/defaults_other.go @@ -1,7 +1,7 @@ //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. diff --git a/src/defaults/defaults_windows.go b/src/config/defaults_windows.go similarity index 97% rename from src/defaults/defaults_windows.go rename to src/config/defaults_windows.go index e2601bf7..5b30b4fd 100644 --- a/src/defaults/defaults_windows.go +++ b/src/config/defaults_windows.go @@ -1,7 +1,7 @@ //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. diff --git a/src/core/api.go b/src/core/api.go index 5accdeee..b5fa93cb 100644 --- a/src/core/api.go +++ b/src/core/api.go @@ -3,43 +3,50 @@ package core import ( "crypto/ed25519" "encoding/json" - "fmt" "net" "net/url" "sync/atomic" "time" "github.com/Arceliar/phony" + + "github.com/Arceliar/ironwood/network" "github.com/yggdrasil-network/yggdrasil-go/src/address" ) type SelfInfo struct { - Key ed25519.PublicKey - Root ed25519.PublicKey - Coords []uint64 + Key ed25519.PublicKey + RoutingEntries uint64 } type PeerInfo struct { - Key ed25519.PublicKey - Root ed25519.PublicKey - Coords []uint64 - Port uint64 - Priority uint8 - Remote string - RXBytes uint64 - TXBytes uint64 - Uptime time.Duration + URI string + Up bool + Inbound bool + LastError error + LastErrorTime time.Time + Key ed25519.PublicKey + Root ed25519.PublicKey + Coords []uint64 + Port uint64 + Priority uint8 + RXBytes uint64 + TXBytes uint64 + Uptime time.Duration } -type DHTEntryInfo struct { - Key ed25519.PublicKey - Port uint64 - Rest uint64 +type TreeEntryInfo struct { + Key ed25519.PublicKey + Parent ed25519.PublicKey + Sequence uint64 + //Port uint64 + //Rest uint64 } type PathEntryInfo struct { - Key ed25519.PublicKey - Path []uint64 + Key ed25519.PublicKey + Path []uint64 + Sequence uint64 } type SessionInfo struct { @@ -53,55 +60,59 @@ 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() []PeerInfo { - var peers []PeerInfo - names := make(map[net.Conn]string) + peers := []PeerInfo{} + conns := map[net.Conn]network.DebugPeerInfo{} + iwpeers := c.PacketConn.PacketConn.Debug.GetPeers() + for _, p := range iwpeers { + conns[p.Conn] = p + } + phony.Block(&c.links, func() { - for _, info := range c.links._links { - if info == nil { - continue + 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.Uptime = time.Since(c.up) } - names[info.conn] = info.lname + if p, ok := conns[conn]; ok { + peerinfo.Key = p.Key + peerinfo.Root = p.Root + peerinfo.Port = p.Port + peerinfo.Priority = p.Priority + } + peers = append(peers, peerinfo) } }) - ps := c.PacketConn.PacketConn.Debug.GetPeers() - for _, p := range ps { - var info PeerInfo - info.Key = p.Key - info.Root = p.Root - info.Coords = p.Coords - info.Port = p.Port - info.Priority = p.Priority - info.Remote = p.Conn.RemoteAddr().String() - if name := names[p.Conn]; name != "" { - info.Remote = name - } - 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() []DHTEntryInfo { - var dhts []DHTEntryInfo - ds := c.PacketConn.PacketConn.Debug.GetDHT() - for _, d := range ds { - var info DHTEntryInfo - 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() []PathEntryInfo { @@ -110,6 +121,7 @@ func (c *Core) GetPaths() []PathEntryInfo { for _, p := range ps { var info PathEntryInfo info.Key = p.Key + info.Sequence = p.Sequence info.Path = p.Path paths = append(paths, info) } @@ -134,16 +146,7 @@ func (c *Core) GetSessions() []SessionInfo { // 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) (*Listener, error) { - switch u.Scheme { - case "tcp": - return c.links.tcp.listen(u, sintf) - case "tls": - return c.links.tls.listen(u, sintf) - case "unix": - return c.links.unix.listen(u, sintf) - default: - return nil, fmt.Errorf("unrecognised scheme %q", u.Scheme) - } + return c.links.listen(u, sintf) } // Address gets the IPv6 address of the Yggdrasil node. This is always a /128 @@ -182,49 +185,14 @@ func (c *Core) SetLogger(log Logger) { // // This adds the peer to the peer list, so that they will be called again if the // connection drops. -func (c *Core) AddPeer(uri string, sourceInterface string) error { - var known bool - phony.Block(c, func() { - _, known = c.config._peers[Peer{uri, sourceInterface}] - }) - if known { - return fmt.Errorf("peer already configured") - } - u, err := url.Parse(uri) - if err != nil { - return err - } - info, err := c.links.call(u, sourceInterface, nil) - if err != nil { - return err - } - phony.Block(c, func() { - c.config._peers[Peer{uri, sourceInterface}] = &info - }) - return nil +func (c *Core) AddPeer(u *url.URL, sintf string) error { + return c.links.add(u, sintf, linkTypePersistent) } // RemovePeer removes a peer. The peer should be specified in URI format, see AddPeer. // The peer is not disconnected immediately. -func (c *Core) RemovePeer(uri string, sourceInterface string) error { - var err error - phony.Block(c, func() { - peer := Peer{uri, sourceInterface} - linkInfo, ok := c.config._peers[peer] - if !ok { - err = fmt.Errorf("peer not configured") - return - } - if ok && linkInfo != nil { - c.links.Act(nil, func() { - if link := c.links._links[*linkInfo]; link != nil { - _ = link.close() - } - }) - } - delete(c.config._peers, peer) - }) - return err +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, @@ -236,8 +204,7 @@ func (c *Core) RemovePeer(uri string, sourceInterface string) error { // 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 { - _, err := c.links.call(u, sintf, nil) - return err + return c.links.add(u, sintf, linkTypeEphemeral) } func (c *Core) PublicKey() ed25519.PublicKey { @@ -274,8 +241,8 @@ func (c *Core) SetAdmin(a AddHandler) error { return err } if err := a.AddHandler( - "debug_remoteGetDHT", "Debug use only", []string{"key"}, - c.proto.getDHTHandler, + "debug_remoteGetTree", "Debug use only", []string{"key"}, + c.proto.getTreeHandler, ); err != nil { return err } diff --git a/src/core/core.go b/src/core/core.go index b096d1d7..79c59d5d 100644 --- a/src/core/core.go +++ b/src/core/core.go @@ -3,6 +3,7 @@ package core import ( "context" "crypto/ed25519" + "crypto/tls" "fmt" "io" "net" @@ -10,10 +11,12 @@ import ( "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/address" "github.com/yggdrasil-network/yggdrasil-go/src/version" ) @@ -34,49 +37,87 @@ type Core struct { log Logger addPeerTimer *time.Timer config struct { - _peers map[Peer]*linkInfo // configurable after startup + tls *tls.Config // immutable after startup + //_peers map[Peer]*linkInfo // configurable after startup _listeners map[ListenAddress]struct{} // configurable 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 New(secret ed25519.PrivateKey, logger Logger, opts ...SetupOption) (*Core, error) { +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(io.Discard, "", 0) + } + if name := version.BuildName(); name != "unknown" { c.log.Infoln("Build name:", name) } if version := version.BuildVersion(); version != "unknown" { c.log.Infoln("Build version:", version) } - c.ctx, c.cancel = context.WithCancel(context.Background()) - // Take a copy of the private key so that it is in our own memory space. - if len(secret) != ed25519.PrivateKeySize { - return nil, fmt.Errorf("private key is incorrect length") - } - c.secret = make(ed25519.PrivateKey, ed25519.PrivateKeySize) - copy(c.secret, secret) - c.public = secret.Public().(ed25519.PublicKey) + var err error - if c.PacketConn, err = iwe.NewPacketConn(c.secret); err != nil { - return nil, fmt.Errorf("error creating encryption: %w", err) - } - c.config._peers = map[Peer]*linkInfo{} c.config._listeners = map[ListenAddress]struct{}{} c.config._allowedPublicKeys = map[[32]byte]struct{}{} for _, opt := range opts { - c._applyOption(opt) + 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 c.log == nil { - c.log = log.New(io.Discard, "", 0) + 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 { 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) } @@ -90,42 +131,18 @@ func New(secret ed25519.PrivateKey, logger Logger, opts ...SetupOption) (*Core, c.log.Errorf("Failed to start listener %q: %s\n", listenaddr, err) } } - c.Act(nil, c._addPeerLoop) return c, nil } -// 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() { - select { - case <-c.ctx.Done(): - return - default: - } - // 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.URI, peer.SourceInterface) // TODO: this should be acted and not in a goroutine? - } - - c.addPeerTimer = time.AfterFunc(time.Minute, func() { - c.Act(nil, c._addPeerLoop) - }) -} - func (c *Core) RetryPeersNow() { - if c.addPeerTimer != nil && !c.addPeerTimer.Stop() { - <-c.addPeerTimer.C - } - c.Act(nil, c._addPeerLoop) + phony.Block(&c.links, func() { + for _, l := range c.links._links { + select { + case l.kick <- struct{}{}: + default: + } + } + }) } // Stop shuts down the Yggdrasil node. @@ -151,11 +168,16 @@ func (c *Core) _close() error { 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) @@ -189,7 +211,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 freeBytes(buf) buf = append(buf, typeSessionTraffic) buf = append(buf, p...) n, err = c.PacketConn.WriteTo(buf, addr) @@ -199,6 +222,20 @@ 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{}) @@ -210,4 +247,5 @@ type Logger 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 8d57f336..cece33c2 100644 --- a/src/core/core_test.go +++ b/src/core/core_test.go @@ -2,14 +2,14 @@ package core import ( "bytes" - "crypto/ed25519" - "math/rand" + "crypto/rand" "net/url" "os" "testing" "time" "github.com/gologme/log" + "github.com/yggdrasil-network/yggdrasil-go/src/config" ) // GetLoggerWithPrefix creates a new logger instance with prefix. @@ -29,29 +29,40 @@ func GetLoggerWithPrefix(prefix string, verbose bool) *log.Logger { // Verbosity flag is passed to logger. func CreateAndConnectTwo(t testing.TB, verbose bool) (nodeA *Core, nodeB *Core) { var err error - var skA, skB ed25519.PrivateKey - if _, skA, err = ed25519.GenerateKey(nil); err != nil { + + cfgA, cfgB := config.GenerateConfig(), config.GenerateConfig() + if err = cfgA.GenerateSelfSignedCertificate(); err != nil { t.Fatal(err) } - if _, skB, err = ed25519.GenerateKey(nil); err != nil { - t.Fatal(err) - } - logger := GetLoggerWithPrefix("", false) - if nodeA, err = New(skA, logger, ListenAddress("tcp://127.0.0.1:0")); err != nil { - t.Fatal(err) - } - if nodeB, err = New(skB, logger, ListenAddress("tcp://127.0.0.1:0")); err != nil { + if err = cfgB.GenerateSelfSignedCertificate(); err != nil { t.Fatal(err) } - u, err := url.Parse("tcp://" + nodeA.links.tcp.getAddr().String()) + 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) + } + + 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) @@ -70,7 +81,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 } } @@ -129,7 +146,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()) @@ -161,7 +178,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()) diff --git a/src/core/debug.go b/src/core/debug.go index 6085ceeb..a6eb5e2a 100644 --- a/src/core/debug.go +++ b/src/core/debug.go @@ -1,6 +1,3 @@ -//go:build debug -// +build debug - package core import ( @@ -8,28 +5,13 @@ import ( "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 fmt.Println(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 933e3983..c3808698 100644 --- a/src/core/link.go +++ b/src/core/link.go @@ -2,11 +2,13 @@ package core import ( "bytes" + "context" "encoding/hex" - "errors" "fmt" "io" + "math" "net" + "net/netip" "net/url" "strconv" "strings" @@ -15,53 +17,74 @@ import ( "github.com/Arceliar/phony" "github.com/yggdrasil-network/yggdrasil-go/src/address" + "golang.org/x/crypto/blake2b" +) + +type linkType int + +const ( + linkTypePersistent linkType = iota // Statically configured + linkTypeEphemeral // Multicast discovered + linkTypeIncoming // Incoming connection ) type links struct { phony.Inbox - core *Core - tcp *linkTCP // TCP interface support - tls *linkTLS // TLS interface support - unix *linkUNIX // UNIX interface support - socks *linkSOCKS // SOCKS interface support + 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 + // _links can only be modified safely from within the links actor _links map[linkInfo]*link // *link is nil if connection in progress } +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 { - linkType string // Type of link, e.g. TCP, AWDL - local string // Local name or address - remote string // Remote name or address -} - -type linkDial struct { - url *url.URL - sintf string + 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 + 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 occured } type linkOptions struct { pinnedEd25519Keys map[keyArray]struct{} priority uint8 + tlsSNI string + password []byte } type Listener struct { - net.Listener - closed chan struct{} + listener net.Listener + ctx context.Context + Cancel context.CancelFunc +} + +func (l *Listener) Addr() net.Addr { + return l.listener.Addr() } func (l *Listener) Close() error { - err := l.Listener.Close() - <-l.closed + l.Cancel() + err := l.listener.Close() + <-l.ctx.Done() return err } @@ -71,6 +94,7 @@ func (l *links) init(c *Core) error { l.tls = l.newLinkTLS(l.tcp) l.unix = l.newLinkUNIX() l.socks = l.newLinkSOCKS() + l.quic = l.newLinkQUIC() l._links = make(map[linkInfo]*link) var listeners []ListenAddress @@ -102,303 +126,502 @@ func (l *links) shutdown() { }) } -func (l *links) isConnectedTo(info linkInfo) bool { - var isConnected bool +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("password is invalid") +const ErrLinkUnrecognisedSchema = linkError("link schema unknown") + +func (l *links) add(u *url.URL, sintf string, linkType linkType) error { + var retErr error phony.Block(l, func() { - _, isConnected = l._links[info] + // 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. + var options linkOptions + 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 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 { + backoff++ + duration := time.Second * time.Duration(math.Exp2(float64(backoff))) + 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 + } + } + + // 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 { + 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. + if err = l.handler(linkType, options, lc); err != nil && err != io.EOF { + l.core.log.Debugf("Link %s error: %s\n", info.uri, err) + } else { + backoff = 0 + } + + // 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 state._err = err; state._err != nil { + 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 + } + return + } + } + }() }) - return isConnected + return retErr } -func (l *links) call(u *url.URL, sintf string, errch chan<- error) (info linkInfo, err error) { - info = linkInfoFor(u.Scheme, sintf, u.Host) - if l.isConnectedTo(info) { - if errch != nil { - close(errch) // already connected, no error +func (l *links) remove(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, } - return info, nil - } - options := linkOptions{ - pinnedEd25519Keys: map[keyArray]struct{}{}, - } - for _, pubkey := range u.Query()["key"] { - sigPub, err := hex.DecodeString(pubkey) - if err != nil { - if errch != nil { - close(errch) + + // 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 info, fmt.Errorf("pinned key contains invalid hex characters") + return } - var sigPubKey keyArray - copy(sigPubKey[:], sigPub) - options.pinnedEd25519Keys[sigPubKey] = struct{}{} + + retErr = ErrLinkNotConfigured + }) + return retErr +} + +func (l *links) listen(u *url.URL, sintf string) (*Listener, error) { + ctx, cancel := 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 + default: + cancel() + return nil, ErrLinkUnrecognisedSchema } + listener, err := protocol.listen(ctx, u, sintf) + if err != nil { + cancel() + return nil, 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 { - if errch != nil { - close(errch) - } - return info, fmt.Errorf("priority invalid: %w", err) + return nil, ErrLinkPriorityInvalid } options.priority = uint8(pi) } - switch info.linkType { + if p := u.Query().Get("password"); p != "" { + if len(p) > blake2b.Size { + return nil, ErrLinkPasswordInvalid + } + options.password = []byte(p) + } + + go func() { + l.core.log.Printf("%s listener started on %s", strings.ToUpper(u.Scheme), listener.Addr()) + defer l.core.log.Printf("%s listener stopped on %s", strings.ToUpper(u.Scheme), listener.Addr()) + for { + conn, err := 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 + }) + if lc == nil { + return + } + + // Give the connection to the handler. The handler will block + // for the lifetime of the connection. + if err = l.handler(linkTypeIncoming, options, lc); err != nil && err != io.EOF { + 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() + phony.Block(l, func() { + if l._links[info] == state { + delete(l._links, info) + } + }) + }(conn) + } + }() + return li, nil +} + +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": - go func() { - if errch != nil { - defer close(errch) - } - if err := l.tcp.dial(u, options, sintf); err != nil && err != io.EOF { - l.core.log.Warnf("Failed to dial TCP %s: %s\n", u.Host, err) - if errch != nil { - errch <- err - } - } - }() - - case "socks": - go func() { - if errch != nil { - defer close(errch) - } - if err := l.socks.dial(u, options); err != nil && err != io.EOF { - l.core.log.Warnf("Failed to dial SOCKS %s: %s\n", u.Host, err) - if errch != nil { - errch <- err - } - } - }() - + dialer = l.tcp case "tls": // 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. - var tlsSNI string if sni := u.Query().Get("sni"); sni != "" { if net.ParseIP(sni) == nil { - 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 tlsSNI == "" { + if options.tlsSNI == "" { if host, _, err := net.SplitHostPort(u.Host); err == nil && net.ParseIP(host) == nil { - tlsSNI = host + options.tlsSNI = host } } - go func() { - if errch != nil { - defer close(errch) - } - if err := l.tls.dial(u, options, sintf, tlsSNI); err != nil && err != io.EOF { - l.core.log.Warnf("Failed to dial TLS %s: %s\n", u.Host, err) - if errch != nil { - errch <- err - } - } - }() - + dialer = l.tls + case "socks": + dialer = l.socks case "unix": - go func() { - if errch != nil { - defer close(errch) - } - if err := l.unix.dial(u, options, sintf); err != nil && err != io.EOF { - l.core.log.Warnf("Failed to dial UNIX %s: %s\n", u.Host, err) - if errch != nil { - errch <- err - } - } - }() - + dialer = l.unix + case "quic": + dialer = l.quic default: - if errch != nil { - close(errch) - } - return info, errors.New("unknown call scheme: " + u.Scheme) + return nil, ErrLinkUnrecognisedSchema } - return info, nil + return dialer.dial(ctx, u, info, options) } -func (l *links) listen(u *url.URL, sintf string) (*Listener, error) { - var listener *Listener - var err error - switch u.Scheme { - case "tcp": - listener, err = l.tcp.listen(u, sintf) - case "tls": - listener, err = l.tls.listen(u, sintf) - case "unix": - listener, err = l.unix.listen(u, sintf) - default: - return nil, fmt.Errorf("unrecognised scheme %q", u.Scheme) - } - return listener, err -} - -func (l *links) create(conn net.Conn, dial *linkDial, name string, info linkInfo, incoming, force bool, options linkOptions) error { - intf := link{ - conn: &linkConn{ - Conn: conn, - up: time.Now(), - }, - lname: name, - links: l, - options: options, - info: info, - incoming: incoming, - force: force, - } - go func() { - if err := intf.handler(dial); err != nil { - l.core.log.Errorf("Link handler %s error (%s): %s", name, conn.RemoteAddr(), err) - } - }() - return nil -} - -func (intf *link) handler(dial *linkDial) error { - defer intf.conn.Close() // nolint:errcheck - - // Don't connect to this link more than once. - if intf.links.isConnectedTo(intf.info) { - return nil - } - - // Mark the connection as in progress. - phony.Block(intf.links, func() { - intf.links._links[intf.info] = nil - }) - - // When we're done, clean up the connection entry. - defer phony.Block(intf.links, func() { - delete(intf.links._links, intf.info) - }) - +func (l *links) handler(linkType linkType, options linkOptions, conn net.Conn) error { meta := version_getBaseMetadata() - meta.key = intf.links.core.public - metaBytes := meta.encode() - if err := intf.conn.SetDeadline(time.Now().Add(time.Second * 6)); err != nil { + meta.publicKey = l.core.public + meta.priority = options.priority + metaBytes, err := meta.encode(l.core.secret, options.password) + if err != nil { + return fmt.Errorf("failed to generate handshake: %w", err) + } + if err := conn.SetDeadline(time.Now().Add(time.Second * 6)); err != nil { return fmt.Errorf("failed to set handshake deadline: %w", err) } - n, err := intf.conn.Write(metaBytes) + n, err := conn.Write(metaBytes) switch { case err != nil: return fmt.Errorf("write handshake: %w", err) case err == nil && n != len(metaBytes): return fmt.Errorf("incomplete handshake send") } - if _, err = io.ReadFull(intf.conn, metaBytes); err != nil { - return fmt.Errorf("read handshake: %w", err) - } - if err = intf.conn.SetDeadline(time.Time{}); err != nil { - return fmt.Errorf("failed to clear handshake deadline: %w", err) - } meta = version_metadata{} base := version_getBaseMetadata() - if !meta.decode(metaBytes) { - return errors.New("failed to decode metadata") + if !meta.decode(conn, options.password) { + return conn.Close() } 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 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; len(pinned) > 0 { + if pinned := options.pinnedEd25519Keys; len(pinned) > 0 { var key keyArray - copy(key[:], meta.key) + copy(key[:], meta.publicKey) if _, allowed := pinned[key]; !allowed { return fmt.Errorf("node public key that does not match pinned keys") } } // Check if we're authorized to connect to this key / IP - allowed := intf.links.core.config._allowedPublicKeys + 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.key) { + if bytes.Equal(k[:], meta.publicKey) { isallowed = true break } } - if intf.incoming && !intf.force && !isallowed { - _ = intf.close() - return fmt.Errorf("node public key %q is not in AllowedPublicKeys", hex.EncodeToString(meta.key)) + if linkType == linkTypeIncoming && !isallowed { + return fmt.Errorf("node public key %q is not in AllowedPublicKeys", hex.EncodeToString(meta.publicKey)) } - phony.Block(intf.links, func() { - intf.links._links[intf.info] = intf - }) - dir := "outbound" - if intf.incoming { + if linkType == linkTypeIncoming { dir = "inbound" } - remoteAddr := net.IP(address.AddrForKey(meta.key)[:]).String() - remoteStr := fmt.Sprintf("%s@%s", remoteAddr, intf.info.remote) - localStr := intf.conn.LocalAddr() - intf.links.core.log.Infof("Connected %s %s: %s, source %s", - dir, strings.ToUpper(intf.info.linkType), remoteStr, localStr) + 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 + } + l.core.log.Infof("Connected %s: %s, source %s", + dir, remoteStr, localStr) - err = intf.links.core.HandleConn(meta.key, intf.conn, intf.options.priority) + err = l.core.HandleConn(meta.publicKey, conn, priority) switch err { case io.EOF, net.ErrClosed, nil: - intf.links.core.log.Infof("Disconnected %s %s: %s, source %s", - dir, strings.ToUpper(intf.info.linkType), remoteStr, localStr) + l.core.log.Infof("Disconnected %s: %s, source %s", + dir, remoteStr, localStr) default: - intf.links.core.log.Infof("Disconnected %s %s: %s, source %s; error: %s", - dir, strings.ToUpper(intf.info.linkType), remoteStr, localStr, err) + l.core.log.Infof("Disconnected %s: %s, source %s; error: %s", + dir, remoteStr, localStr, err) } - - if !intf.incoming && dial != nil { - // The connection was one that we dialled, so wait a second and try to - // dial it again. - var retry func(attempt int) - retry = func(attempt int) { - // intf.links.core.log.Infof("Retrying %s (attempt %d of 5)...", dial.url.String(), attempt) - errch := make(chan error, 1) - if _, err := intf.links.call(dial.url, dial.sintf, errch); err != nil { - return - } - if err := <-errch; err != nil { - if attempt < 3 { - time.AfterFunc(time.Second, func() { - retry(attempt + 1) - }) - } - } - } - time.AfterFunc(time.Second, func() { - retry(1) - }) - } - return nil } -func (intf *link) close() error { - return intf.conn.Close() -} - -func linkInfoFor(linkType, sintf, remote string) linkInfo { - return linkInfo{ - linkType: linkType, - local: sintf, - remote: remote, +func urlForLinkInfo(u url.URL) url.URL { + u.RawQuery = "" + if host, _, err := net.SplitHostPort(u.Host); err == nil { + if addr, err := netip.ParseAddr(host); err == nil { + // For peers that look like multicast peers (i.e. + // link-local addresses), we will ignore the port number, + // otherwise we might open multiple connections to them. + if addr.IsLinkLocalUnicast() { + u.Host = fmt.Sprintf("[%s]", addr.String()) + } + } } + return u } type linkConn struct { @@ -421,12 +644,3 @@ func (c *linkConn) Write(p []byte) (n int, err error) { atomic.AddUint64(&c.tx, uint64(n)) return } - -func linkOptionsForListener(u *url.URL) (l linkOptions) { - if p := u.Query().Get("priority"); p != "" { - if pi, err := strconv.ParseUint(p, 10, 8); err == nil { - l.priority = uint8(pi) - } - } - return -} diff --git a/src/core/link_quic.go b/src/core/link_quic.go new file mode 100644 index 00000000..976aba6a --- /dev/null +++ b/src/core/link_quic.go @@ -0,0 +1,96 @@ +package core + +import ( + "context" + "crypto/tls" + "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.EarlyListener + 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) { + qc, err := quic.DialAddrEarly(ctx, url.Host, l.tlsconfig, l.quicconfig) + if err != nil { + return nil, err + } + qs, err := qc.OpenStream() + 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.ListenAddrEarly(url.Host, l.tlsconfig, l.quicconfig) + if err != nil { + return nil, err + } + ch := make(chan *linkQUICStream) + lql := &linkQUICListener{ + EarlyListener: ql, + ch: ch, + } + go func() { + for { + qc, err := ql.Accept(ctx) + if err != nil { + ql.Close() + return + } + qs, err := qc.AcceptStream(ctx) + if err != nil { + ql.Close() + return + } + ch <- &linkQUICStream{ + Connection: qc, + Stream: qs, + } + } + }() + return lql, nil +} diff --git a/src/core/link_socks.go b/src/core/link_socks.go index 4cdffa51..1a038fa6 100644 --- a/src/core/link_socks.go +++ b/src/core/link_socks.go @@ -1,6 +1,7 @@ package core import ( + "context" "fmt" "net" "net/url" @@ -20,37 +21,22 @@ func (l *links) newLinkSOCKS() *linkSOCKS { return lt } -func (l *linkSOCKS) dial(url *url.URL, options linkOptions) error { - info := linkInfoFor("socks", "", url.Path) - if l.links.isConnectedTo(info) { - return nil +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() } - proxyAuth := &proxy.Auth{} - proxyAuth.User = url.User.Username() - proxyAuth.Password, _ = url.User.Password() dialer, err := proxy.SOCKS5("tcp", url.Host, proxyAuth, proxy.Direct) if err != nil { - return fmt.Errorf("failed to configure proxy") + return nil, fmt.Errorf("failed to configure proxy") } pathtokens := strings.Split(strings.Trim(url.Path, "/"), "/") - conn, err := dialer.Dial("tcp", pathtokens[0]) - if err != nil { - return err - } - dial := &linkDial{ - url: url, - } - return l.handler(dial, info, conn, options, false) + return dialer.Dial("tcp", pathtokens[0]) } -func (l *linkSOCKS) handler(dial *linkDial, info linkInfo, conn net.Conn, options linkOptions, incoming bool) error { - return l.links.create( - conn, // connection - dial, // connection URL - dial.url.String(), // connection name - info, // connection info - incoming, // not incoming - false, // not forced - options, // connection options - ) +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 index 714ea9d9..f595aeb9 100644 --- a/src/core/link_tcp.go +++ b/src/core/link_tcp.go @@ -5,7 +5,7 @@ import ( "fmt" "net" "net/url" - "strings" + "strconv" "time" "github.com/Arceliar/phony" @@ -14,117 +14,88 @@ import ( type linkTCP struct { phony.Inbox *links - listener *net.ListenConfig - _listeners map[*Listener]context.CancelFunc + listenconfig *net.ListenConfig + _listeners map[*Listener]context.CancelFunc } func (l *links) newLinkTCP() *linkTCP { lt := &linkTCP{ links: l, - listener: &net.ListenConfig{ + listenconfig: &net.ListenConfig{ KeepAlive: -1, }, _listeners: map[*Listener]context.CancelFunc{}, } - lt.listener.Control = lt.tcpContext + lt.listenconfig.Control = lt.tcpContext return lt } -func (l *linkTCP) dial(url *url.URL, options linkOptions, sintf string) error { - addr, err := net.ResolveTCPAddr("tcp", url.Host) - if err != nil { - return err - } - dialer, err := l.dialerFor(addr, sintf) - if err != nil { - return err - } - info := linkInfoFor("tcp", sintf, tcpIDFor(dialer.LocalAddr, addr)) - if l.links.isConnectedTo(info) { - return nil - } - conn, err := dialer.DialContext(l.core.ctx, "tcp", addr.String()) - if err != nil { - return err - } - name := strings.TrimRight(strings.SplitN(url.String(), "?", 2)[0], "/") - dial := &linkDial{ - url: url, - sintf: sintf, - } - return l.handler(dial, name, info, conn, options, false, false) +type tcpDialer struct { + info linkInfo + dialer *net.Dialer + addr *net.TCPAddr } -func (l *linkTCP) listen(url *url.URL, sintf string) (*Listener, error) { - ctx, cancel := context.WithCancel(l.core.ctx) +func (l *linkTCP) dialersFor(url *url.URL, info linkInfo) ([]*tcpDialer, error) { + host, p, err := net.SplitHostPort(url.Host) + if err != nil { + return nil, err + } + port, err := strconv.Atoi(p) + if err != nil { + return nil, err + } + ips, err := net.LookupIP(host) + if err != nil { + return nil, err + } + dialers := make([]*tcpDialer, 0, len(ips)) + for _, ip := range ips { + addr := &net.TCPAddr{ + IP: ip, + Port: port, + } + dialer, err := l.dialerFor(addr, info.sintf) + if err != nil { + continue + } + dialers = append(dialers, &tcpDialer{ + info: info, + dialer: dialer, + addr: addr, + }) + } + return dialers, nil +} + +func (l *linkTCP) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) { + dialers, err := l.dialersFor(url, info) + if err != nil { + return nil, err + } + if len(dialers) == 0 { + return nil, nil + } + for _, d := range dialers { + var conn net.Conn + conn, err = d.dialer.DialContext(ctx, "tcp", d.addr.String()) + if err != nil { + l.core.log.Warnf("Failed to connect to %s: %s", d.addr, err) + continue + } + return conn, nil + } + return nil, err +} + +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) } } - listener, err := l.listener.Listen(ctx, "tcp", hostport) - if err != nil { - cancel() - return nil, err - } - entry := &Listener{ - Listener: listener, - closed: make(chan struct{}), - } - phony.Block(l, func() { - l._listeners[entry] = cancel - }) - l.core.log.Printf("TCP listener started on %s", listener.Addr()) - go func() { - defer phony.Block(l, func() { - delete(l._listeners, entry) - }) - for { - conn, err := listener.Accept() - if err != nil { - cancel() - break - } - laddr := conn.LocalAddr().(*net.TCPAddr) - raddr := conn.RemoteAddr().(*net.TCPAddr) - name := fmt.Sprintf("tcp://%s", raddr) - info := linkInfoFor("tcp", sintf, tcpIDFor(laddr, raddr)) - if err = l.handler(nil, name, info, conn, linkOptionsForListener(url), true, raddr.IP.IsLinkLocalUnicast()); err != nil { - l.core.log.Errorln("Failed to create inbound link:", err) - } - } - _ = listener.Close() - close(entry.closed) - l.core.log.Printf("TCP listener stopped on %s", listener.Addr()) - }() - return entry, nil -} - -func (l *linkTCP) handler(dial *linkDial, name string, info linkInfo, conn net.Conn, options linkOptions, incoming, force bool) error { - return l.links.create( - conn, // connection - dial, // connection URL - name, // connection name - info, // connection info - incoming, // not incoming - force, // not forced - options, // connection options - ) -} - -// Returns the address of the listener. -func (l *linkTCP) 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 - var addr *net.TCPAddr - phony.Block(l, func() { - for listener := range l._listeners { - addr = listener.Addr().(*net.TCPAddr) - } - }) - return addr + return l.listenconfig.Listen(ctx, "tcp", hostport) } func (l *linkTCP) dialerFor(dst *net.TCPAddr, sintf string) (*net.Dialer, error) { @@ -185,16 +156,3 @@ func (l *linkTCP) dialerFor(dst *net.TCPAddr, sintf string) (*net.Dialer, error) } return dialer, nil } - -func tcpIDFor(local net.Addr, remoteAddr *net.TCPAddr) string { - if localAddr, ok := local.(*net.TCPAddr); ok && localAddr.IP.Equal(remoteAddr.IP) { - // Nodes running on the same host — include both the IP and port. - return remoteAddr.String() - } - if remoteAddr.IP.IsLinkLocalUnicast() { - // Nodes discovered via multicast — include the IP only. - return remoteAddr.IP.String() - } - // Nodes connected remotely — include both the IP and port. - return remoteAddr.String() -} diff --git a/src/core/link_tcp_linux.go b/src/core/link_tcp_linux.go index 9e875fee..6c54f30b 100644 --- a/src/core/link_tcp_linux.go +++ b/src/core/link_tcp_linux.go @@ -12,22 +12,6 @@ import ( // 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 { - 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 } diff --git a/src/core/link_tls.go b/src/core/link_tls.go index dda0e2fe..a93227f6 100644 --- a/src/core/link_tls.go +++ b/src/core/link_tls.go @@ -1,20 +1,11 @@ package core import ( - "bytes" "context" - "crypto/rand" "crypto/tls" - "crypto/x509" - "crypto/x509/pkix" - "encoding/hex" - "encoding/pem" "fmt" - "math/big" "net" "net/url" - "strings" - "time" "github.com/Arceliar/phony" ) @@ -36,49 +27,38 @@ func (l *links) newLinkTLS(tcp *linkTCP) *linkTLS { Control: tcp.tcpContext, KeepAlive: -1, }, + config: l.core.config.tls.Clone(), _listeners: map[*Listener]context.CancelFunc{}, } - var err error - lt.config, err = lt.generateConfig() - if err != nil { - panic(err) - } return lt } -func (l *linkTLS) dial(url *url.URL, options linkOptions, sintf, sni string) error { - addr, err := net.ResolveTCPAddr("tcp", url.Host) +func (l *linkTLS) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) { + dialers, err := l.tcp.dialersFor(url, info) if err != nil { - return err + return nil, err } - dialer, err := l.tcp.dialerFor(addr, sintf) - if err != nil { - return err + if len(dialers) == 0 { + return nil, nil } - info := linkInfoFor("tls", sintf, tcpIDFor(dialer.LocalAddr, addr)) - if l.links.isConnectedTo(info) { - return nil + for _, d := range dialers { + tlsconfig := l.config.Clone() + tlsconfig.ServerName = options.tlsSNI + tlsdialer := &tls.Dialer{ + NetDialer: d.dialer, + Config: tlsconfig, + } + var conn net.Conn + conn, err = tlsdialer.DialContext(ctx, "tcp", d.addr.String()) + if err != nil { + continue + } + return conn, nil } - tlsconfig := l.config.Clone() - tlsconfig.ServerName = sni - tlsdialer := &tls.Dialer{ - NetDialer: dialer, - Config: tlsconfig, - } - conn, err := tlsdialer.DialContext(l.core.ctx, "tcp", addr.String()) - if err != nil { - return err - } - name := strings.TrimRight(strings.SplitN(url.String(), "?", 2)[0], "/") - dial := &linkDial{ - url: url, - sintf: sintf, - } - return l.handler(dial, name, info, conn, options, false, false) + return nil, err } -func (l *linkTLS) listen(url *url.URL, sintf string) (*Listener, error) { - ctx, cancel := context.WithCancel(l.core.ctx) +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 { @@ -87,88 +67,8 @@ func (l *linkTLS) listen(url *url.URL, sintf string) (*Listener, error) { } listener, err := l.listener.Listen(ctx, "tcp", hostport) if err != nil { - cancel() return nil, err } tlslistener := tls.NewListener(listener, l.config) - entry := &Listener{ - Listener: tlslistener, - closed: make(chan struct{}), - } - phony.Block(l, func() { - l._listeners[entry] = cancel - }) - l.core.log.Printf("TLS listener started on %s", listener.Addr()) - go func() { - defer phony.Block(l, func() { - delete(l._listeners, entry) - }) - for { - conn, err := tlslistener.Accept() - if err != nil { - cancel() - break - } - laddr := conn.LocalAddr().(*net.TCPAddr) - raddr := conn.RemoteAddr().(*net.TCPAddr) - name := fmt.Sprintf("tls://%s", raddr) - info := linkInfoFor("tls", sintf, tcpIDFor(laddr, raddr)) - if err = l.handler(nil, name, info, conn, linkOptionsForListener(url), true, raddr.IP.IsLinkLocalUnicast()); err != nil { - l.core.log.Errorln("Failed to create inbound link:", err) - } - } - _ = tlslistener.Close() - close(entry.closed) - l.core.log.Printf("TLS listener stopped on %s", listener.Addr()) - }() - return entry, nil -} - -// RFC5280 section 4.1.2.5 -var notAfterNeverExpires = time.Date(9999, time.December, 31, 23, 59, 59, 0, time.UTC) - -func (l *linkTLS) generateConfig() (*tls.Config, error) { - certBuf := &bytes.Buffer{} - cert := x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{ - CommonName: hex.EncodeToString(l.links.core.public[:]), - }, - 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, l.links.core.public, l.links.core.secret) - if err != nil { - return nil, err - } - - if err := pem.Encode(certBuf, &pem.Block{ - Type: "CERTIFICATE", - Bytes: certbytes, - }); err != nil { - return nil, err - } - - rootCAs := x509.NewCertPool() - rootCAs.AppendCertsFromPEM(certbytes) - - return &tls.Config{ - RootCAs: rootCAs, - Certificates: []tls.Certificate{ - { - Certificate: [][]byte{certbytes}, - PrivateKey: l.links.core.secret, - }, - }, - InsecureSkipVerify: true, - MinVersion: tls.VersionTLS13, - }, nil -} - -func (l *linkTLS) handler(dial *linkDial, name string, info linkInfo, conn net.Conn, options linkOptions, incoming, force bool) error { - return l.tcp.handler(dial, name, info, conn, options, incoming, force) + return tlslistener, nil } diff --git a/src/core/link_unix.go b/src/core/link_unix.go index 7f782575..8dde8946 100644 --- a/src/core/link_unix.go +++ b/src/core/link_unix.go @@ -32,70 +32,14 @@ func (l *links) newLinkUNIX() *linkUNIX { return lt } -func (l *linkUNIX) dial(url *url.URL, options linkOptions, _ string) error { - info := linkInfoFor("unix", "", url.Path) - if l.links.isConnectedTo(info) { - return nil - } +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 err - } - conn, err := l.dialer.DialContext(l.core.ctx, "unix", addr.String()) - if err != nil { - return err - } - dial := &linkDial{ - url: url, - } - return l.handler(dial, url.String(), info, conn, options, false) -} - -func (l *linkUNIX) listen(url *url.URL, _ string) (*Listener, error) { - ctx, cancel := context.WithCancel(l.core.ctx) - listener, err := l.listener.Listen(ctx, "unix", url.Path) - if err != nil { - cancel() return nil, err } - entry := &Listener{ - Listener: listener, - closed: make(chan struct{}), - } - phony.Block(l, func() { - l._listeners[entry] = cancel - }) - l.core.log.Printf("UNIX listener started on %s", listener.Addr()) - go func() { - defer phony.Block(l, func() { - delete(l._listeners, entry) - }) - for { - conn, err := listener.Accept() - if err != nil { - cancel() - break - } - info := linkInfoFor("unix", "", url.String()) - if err = l.handler(nil, url.String(), info, conn, linkOptionsForListener(url), true); err != nil { - l.core.log.Errorln("Failed to create inbound link:", err) - } - } - _ = listener.Close() - close(entry.closed) - l.core.log.Printf("UNIX listener stopped on %s", listener.Addr()) - }() - return entry, nil + return l.dialer.DialContext(ctx, "unix", addr.String()) } -func (l *linkUNIX) handler(dial *linkDial, name string, info linkInfo, conn net.Conn, options linkOptions, incoming bool) error { - return l.links.create( - conn, // connection - dial, // connection URL - name, // connection name - info, // connection info - incoming, // not incoming - false, // not forced - options, // connection options - ) +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/options.go b/src/core/options.go index 66aa16ce..ffbdae73 100644 --- a/src/core/options.go +++ b/src/core/options.go @@ -2,12 +2,18 @@ package core import ( "crypto/ed25519" + "fmt" + "net/url" ) -func (c *Core) _applyOption(opt SetupOption) { +func (c *Core) _applyOption(opt SetupOption) (err error) { switch v := opt.(type) { case Peer: - c.config._peers[v] = nil + u, err := url.Parse(v.URI) + if err != nil { + return fmt.Errorf("unable to parse peering URI: %w", err) + } + return c.links.add(u, v.SourceInterface, linkTypePersistent) case ListenAddress: c.config._listeners[v] = struct{}{} case NodeInfo: @@ -19,6 +25,7 @@ func (c *Core) _applyOption(opt SetupOption) { copy(pk[:], v) c.config._allowedPublicKeys[pk] = struct{}{} } + return } type SetupOption interface { 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..d8cae737 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) } } @@ -322,16 +322,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 } @@ -343,7 +343,7 @@ func (p *protoHandler) getDHTHandler(in json.RawMessage) (interface{}, error) { } 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) @@ -367,7 +367,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/tls.go b/src/core/tls.go new file mode 100644 index 00000000..0ac7e87a --- /dev/null +++ b/src/core/tls.go @@ -0,0 +1,29 @@ +package core + +import ( + "crypto/tls" + "crypto/x509" +) + +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 + }, + VerifyPeerCertificate: c.verifyTLSCertificate, + VerifyConnection: c.verifyTLSConnection, + InsecureSkipVerify: true, + MinVersion: tls.VersionTLS13, + } + return config, nil +} + +func (c *Core) verifyTLSCertificate(_ [][]byte, _ [][]*x509.Certificate) error { + return nil +} + +func (c *Core) verifyTLSConnection(_ tls.ConnectionState) error { + return nil +} diff --git a/src/core/version.go b/src/core/version.go index 0bfbbcbc..332e18c8 100644 --- a/src/core/version.go +++ b/src/core/version.go @@ -4,65 +4,154 @@ 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" + "fmt" + "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 +) + // 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, fmt.Errorf("hash writer only wrote %d bytes", n) + } + 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() { +func (m *version_metadata) decode(r io.Reader, password []byte) bool { + bh := [6]byte{} + if _, err := io.ReadFull(r, bh[:]); err != nil { return false } - 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 false + } + bs := make([]byte, binary.BigEndian.Uint16(bh[4:6])) + if _, err := io.ReadFull(r, bs); err != nil { + return false + } + + if len(bs) < ed25519.SignatureSize { + return false + } + 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 false + } + n, err := hasher.Write(m.publicKey) + if err != nil || n != ed25519.PublicKeySize { + return false + } + hash := hasher.Sum(nil) + return ed25519.Verify(m.publicKey, hash, sig) } // 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..7efe2f35 --- /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); 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 !decoded.decode(encoded, password) { + t.Fatalf("failed to decode") + } + 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 6374f4ed..00000000 --- a/src/defaults/defaults.go +++ /dev/null @@ -1,60 +0,0 @@ -package defaults - -import "github.com/yggdrasil-network/yggdrasil-go/src/config" - -type MulticastInterfaceConfig = config.MulticastInterfaceConfig - -var defaultConfig = "" // LDFLAGS='-X github.com/yggdrasil-network/yggdrasil-go/src/defaults.defaultConfig=/path/to/config -var defaultAdminListen = "" // LDFLAGS='-X github.com/yggdrasil-network/yggdrasil-go/src/defaults.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 -} - -// 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 { - // Get the defaults for the platform. - defaults := GetDefaults() - // Create a node configuration and populate it. - cfg := new(config.NodeConfig) - cfg.NewKeys() - 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 - - 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/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 ec145230..741c4315 100644 --- a/src/multicast/multicast.go +++ b/src/multicast/multicast.go @@ -4,9 +4,9 @@ import ( "bytes" "context" "crypto/ed25519" - "encoding/binary" "encoding/hex" "fmt" + "math/rand" "net" "net/url" "time" @@ -15,6 +15,7 @@ import ( "github.com/gologme/log" "github.com/yggdrasil-network/yggdrasil-go/src/core" + "golang.org/x/crypto/blake2b" "golang.org/x/net/ipv6" ) @@ -44,6 +45,8 @@ type interfaceInfo struct { listen bool port uint16 priority uint8 + password []byte + hash []byte } type listenerInfo struct { @@ -177,6 +180,7 @@ func (m *Multicast) _getAllowedInterfaces() map[string]*interfaceInfo { return nil } // Work out which interfaces to announce on + pk := m.core.PublicKey() for _, iface := range allifaces { switch { case iface.Flags&net.FlagUp == 0: @@ -195,12 +199,23 @@ func (m *Multicast) _getAllowedInterfaces() map[string]*interfaceInfo { 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 } @@ -248,7 +263,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 @@ -295,12 +310,15 @@ 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 { m.log.Debugln("Started multicasting on", iface.Name) @@ -321,17 +339,21 @@ 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 @@ -339,7 +361,8 @@ func (m *Multicast) _announce() { break } } - m._timer = 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) }) } @@ -350,8 +373,9 @@ func (m *Multicast) listen() { 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) + n, rcm, fromAddr, err := m.sock.ReadFrom(bs) if err != nil { if !m.IsStarted() { return @@ -369,40 +393,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 - } + 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&priority=%d", hex.EncodeToString(key), info.priority) - 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/options.go b/src/multicast/options.go index f36284ed..bd9fea5a 100644 --- a/src/multicast/options.go +++ b/src/multicast/options.go @@ -21,6 +21,7 @@ type MulticastInterface struct { Listen bool Port uint16 Priority uint8 + Password string } type GroupAddress string diff --git a/src/tun/options.go b/src/tun/options.go index 7be79211..58d3d80c 100644 --- a/src/tun/options.go +++ b/src/tun/options.go @@ -6,6 +6,8 @@ func (m *TunAdapter) _applyOption(opt SetupOption) { m.config.name = v case InterfaceMTU: m.config.mtu = v + case FileDescriptor: + m.config.fd = int32(v) } } @@ -15,6 +17,8 @@ type SetupOption interface { type InterfaceName string type InterfaceMTU uint64 +type FileDescriptor int32 -func (a InterfaceName) isSetupOption() {} -func (a InterfaceMTU) isSetupOption() {} +func (a InterfaceName) isSetupOption() {} +func (a InterfaceMTU) isSetupOption() {} +func (a FileDescriptor) isSetupOption() {} diff --git a/src/tun/tun.go b/src/tun/tun.go index ce1bd169..83c82670 100644 --- a/src/tun/tun.go +++ b/src/tun/tun.go @@ -8,42 +8,50 @@ package tun import ( "errors" "fmt" + "io" "net" "github.com/Arceliar/phony" "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/core" - "github.com/yggdrasil-network/yggdrasil-go/src/defaults" - "github.com/yggdrasil-network/yggdrasil-go/src/ipv6rwc" ) 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 + rwc ReadWriteCloser log core.Logger addr address.Address subnet address.Subnet mtu uint64 iface tun.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 - config struct { + isOpen bool + isEnabled bool // Used by the writer to drop sessionTraffic if not enabled + config struct { + fd int32 name InterfaceName mtu InterfaceMTU } } // 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 @@ -72,25 +80,25 @@ 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 New(rwc *ipv6rwc.ReadWriteCloser, log core.Logger, opts ...SetupOption) (*TunAdapter, error) { +func New(rwc ReadWriteCloser, log core.Logger, opts ...SetupOption) (*TunAdapter, error) { tun := &TunAdapter{ rwc: rwc, log: log, @@ -107,7 +115,11 @@ func (tun *TunAdapter) _start() error { } 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) + 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 @@ -118,7 +130,13 @@ func (tun *TunAdapter) _start() error { if tun.rwc.MaxMTU() < mtu { mtu = tun.rwc.MaxMTU() } - if err := tun.setup(string(tun.config.name), 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 { diff --git a/src/tun/tun_bsd.go b/src/tun/tun_bsd.go index 9a8f70ce..5f42f52b 100644 --- a/src/tun/tun_bsd.go +++ b/src/tun/tun_bsd.go @@ -5,6 +5,7 @@ package tun import ( "encoding/binary" + "fmt" "os/exec" "strconv" "strings" @@ -85,7 +86,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 { diff --git a/src/tun/tun_darwin.go b/src/tun/tun_darwin.go index a6d87a0c..8fde93a1 100644 --- a/src/tun/tun_darwin.go +++ b/src/tun/tun_darwin.go @@ -1,5 +1,5 @@ -//go:build !mobile -// +build !mobile +//go:build darwin || ios +// +build darwin ios package tun @@ -7,6 +7,7 @@ package tun import ( "encoding/binary" + "os" "strconv" "strings" "unsafe" @@ -31,7 +32,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 err + } + err = unix.SetNonblock(dfd, true) + if err != nil { + unix.Close(dfd) + return err + } + iface, err := wgtun.CreateTUNFromFile(os.NewFile(uintptr(dfd), "/dev/tun"), 0) + if err != nil { + unix.Close(dfd) + return 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 ( @@ -117,13 +146,13 @@ 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 } - 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 diff --git a/src/tun/tun_linux.go b/src/tun/tun_linux.go index 1e42b7b8..9cf21446 100644 --- a/src/tun/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 tun // The linux platform specific tun parts import ( + "fmt" + "github.com/vishvananda/netlink" wgtun "golang.zx2c4.com/wireguard/tun" ) @@ -25,7 +27,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") } // Configures the TUN adapter with the correct IPv6 address and MTU. Netlink diff --git a/src/tun/tun_other.go b/src/tun/tun_other.go index c618d837..bf8ac892 100644 --- a/src/tun/tun_other.go +++ b/src/tun/tun_other.go @@ -1,5 +1,5 @@ -//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 tun @@ -7,6 +7,8 @@ package tun // If your platform supports tun devices, you could try configuring it manually import ( + "fmt" + wgtun "golang.zx2c4.com/wireguard/tun" ) @@ -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/tun/tun_windows.go b/src/tun/tun_windows.go index c3e36596..19d9248c 100644 --- a/src/tun/tun_windows.go +++ b/src/tun/tun_windows.go @@ -4,12 +4,12 @@ package tun import ( - "bytes" "errors" + "fmt" "log" - "net" + "net/netip" - "github.com/yggdrasil-network/yggdrasil-go/src/defaults" + "github.com/yggdrasil-network/yggdrasil-go/src/config" "golang.org/x/sys/windows" wgtun "golang.zx2c4.com/wireguard/tun" @@ -22,7 +22,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 @@ -35,9 +35,11 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error { return err } tun.iface = iface - if err = tun.setupAddress(addr); err != nil { - tun.log.Errorln("Failed to set up TUN address:", err) - return err + if addr != "" { + 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) @@ -50,6 +52,11 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error { }) } +// 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() == "" { @@ -83,13 +90,9 @@ func (tun *TunAdapter) setupAddress(addr string) error { 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 +115,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 +132,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) } } }