Conversation
3ab1f42 to
437064c
Compare
| end | ||
| def initialize(@version) | ||
| @ts = Time.utc.to_unix_ms | ||
| @nounce = uninitialized UInt8[16] |
There was a problem hiding this comment.
I know it's not introduced here but stack allocated variables (that are not copied) are dangerous to use as instances variables, pointer to unallocated stack memory will result in a seg fault
| @nounce = uninitialized UInt8[16] | |
| @nounce = Bytes.new 16 |
src/message.cr
Outdated
| String.build(39) do |str| | ||
| 8.times do |i| | ||
| str << ':' unless i == 0 | ||
| str << @ip[i * 2].to_s(16).rjust(2, '0') |
There was a problem hiding this comment.
This allocated two new strings and then concat it to the third.
| str << @ip[i * 2].to_s(16).rjust(2, '0') | |
| str << '0' if @ip[i * 2] < 0x10 | |
| @ip[i * 2].to_s(str, 16) |
src/message.cr
Outdated
| self.new(version, ts, nounce, ip) | ||
| struct V2 < Base | ||
| getter family : UInt8 | ||
| getter ip : StaticArray(UInt8, 16) | StaticArray(UInt8, 4) |
There was a problem hiding this comment.
Consider if it should be a Bytes instead
|
Are both the server and client backward compatible? |
The server is backwards compatible. I started to look at the client but couldn't figure out a good solution for that, so it's still in the works! Would it be reasonable to add a CLI arg to the client to enforce ipv4 (version 1)? Or maybe always resort to a v1 message fallback at the end of this list? See comment at the end of the method. # Send to all resolved IPs for the hostname
private def self.udp_send(host, port, data) : Array(String)
host_addresses = Socket::Addrinfo.udp(host, port, Socket::Family::INET)
socket = Socket.udp(Socket::Family::INET)
Socket.set_blocking(socket.fd, true)
host_addresses.each do |addrinfo|
begin
socket.send data, to: addrinfo.ip_address
rescue ex
STDERR << "Sparoid error sending " << ex.inspect << "\n"
end
end
# Loop over all host_addresses using IPv4
# Create a V1 message and send to all those hosts
host_addresses.map &.ip_address.address
ensure
socket.close if socket
end |
There was a problem hiding this comment.
Pull request overview
Adds IPv6-awareness to Sparoid’s UDP message format and client/server plumbing so receivers can distinguish IPv4 vs IPv6 sources and apply different acceptance actions (e.g., nftables rules).
Changes:
- Introduces
Sparoid::Messageversioning with new V2 format (adds IP family + optional CIDR range; supports IPv6). - Updates server callback signature to include
Socket::Family, and extends CLI/config to optionally run a separatenftablesv6-cmd. - Updates client to generate/send multiple messages (V1 + V2) and adds IPv6 address discovery helpers + expanded specs/docs.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/server.cr | Updates server initialization/binding and passes IP family to accept callback. |
| src/server-cli.cr | Adds nftables IPv6 command path selection based on accepted packet family. |
| src/public_ip.cr | Changes public IP discovery to return IPv4/IPv6 strings and caches multiple results. |
| src/message.cr | Refactors message into versioned formats (V1 + new V2 with family/range + IPv6 string formatting). |
| src/ipv6.cr | Adds IPv6 interface inspection + CIDR inference via getifaddrs. |
| src/config.cr | Adds nftablesv6-cmd config parsing. |
| src/client.cr | Sends V1+V2 messages, prioritizes IPv6 resolution, and integrates IPv6 discovery. |
| spec/sparoid_spec.cr | Adjusts expectations for multiple packets per send and updated callback signature. |
| spec/message_spec.cr | Adds comprehensive tests for V1/V2 parsing/serialization and IPv6 formatting. |
| README.md | Updates example configuration and nftables rules for IPv6 support. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…low for ipv6 addresses.
bfa0286 to
564e140
Compare
oskgu360
left a comment
There was a problem hiding this comment.
Looks good, Server team tested it out on a CloudAMQP machine to verify its backwardscompatible to the old clients (ruby and crystal). And working with ipv6.
My mind got stuck on what a Message really is, so wanted to rename it to something else more decriptive, but messy to change and I have no good suggestions (PortKnockingProtocolPacket? 😀 ). So maybe better to just keep, once I got familiar with the code, it made sense.
I have hard time giving a proper review on the low level stuff, so if you want opinions there, someone else has to hehe. But it works, and this app isn't that performance sensitive, so good to go imo.
Good job! 🚀
| nftables-cmd = add element inet filter sparoid { %s } | ||
| nftablesv6-cmd = add element inet filter sparoid6 { %s } |
There was a problem hiding this comment.
nftables-cmd = add element inet filter sparoid%{v} { %s }
on_accept = ->(ip_str : String, family : Socket::Family) : Nil {
version_suffix = family == Socket::Family::INET6 ? "6" : ""
cmd = sprintf(c.nftables_cmd.gsub("%{v}", version_suffix), ip_str)
nft.run_cmd(cmd)
}
I think we could do something like this if we would like to reduce config options, but I guess its clearer seperating them anyway
|
About the client/server compatibility, Currently, both the client and server are backwards compatible with each other—a new client can speak to an old server, and vice versa. This increases complexity, since clients must handle both old and new handshake/nonce formats. It can become very complex and limiting if we always want to keep 2-way comparability. Do we actually need this? If we instead require that servers are always upgraded before clients (which is quite common), we could simplify things so only servers are backwards compatible:
Would this be a reasonable change to make, so clients only need to send the v2 format? |
Well, I had that solution in the first iteration of the PR. Can't really seem to figure out why I went with the client keep sending the v1 format if I'm being honest. Maybe the following comment tricked me :D
If there is a problem with the client sending the v2 format all it has to do is to downgrade the client version. Let's revert the client changes to only send v2 messages. |
Remove v1 message generation from the client. Servers must be upgraded before clients to maintain backwards compatibility.
src/client.cr
Outdated
| end | ||
|
|
||
| private def self.local_ips(host : Socket::IPAddress) : Array(Bytes) | ||
| ipv4 = Slice[127u8, 0u8, 0u8, 1u8] |
Replace slice_to_bytes with encode_ip that pattern-matches on StaticArray types, and simplify generate_messages control flow.
Remove the unused `by_dns` method and move string stripping into `by_http` and `read_cache` so callers don't need to handle it. Change URLS to a Tuple and simplify cache reading.
|
I pushed a couple of suggestions. But I'm also thinking if we can simply quite a lot by always sending a IPv6 address ( |
IPv4 addresses are stored as IPv4-mapped IPv6 (::ffff:x.x.x.x). Family is now a computed property checking the prefix instead of a stored field. Added from_ip overloads for StaticArray(UInt8, 4), StaticArray(UInt8, 16), and StaticArray(UInt16, 8), removing the encode_ip helper from the client.
IPv4 ranges are stored +96 (e.g. /32 becomes /128) to match the IPv4-mapped IPv6 address. The range getter subtracts 96 for IPv4, and serialization always writes family=6 with all 16 IP bytes. Old family=4 messages are still handled in from_io for compat.
Replace Channel-based fiber synchronization with WaitGroup in fdpass. Simplify UDPSocket creation and remove redundant .try on close. Relax public_ip parameter type and add comments to generate_messages.
Move IP string parsing into Message::V2.from_ip(String) so client.cr can pass IP strings directly instead of parsing them first. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WHY are these changes introduced?
We want to support ipv6
WHAT is this pull request doing?
Adds a new message format that adds support for ipv6.
The message format is using the same initial format as v1 but adds a ip family byte before the IP and support for a range.
version + timestamp + nonce + family (ipv4 or ipv6) (`4`/`6`) + ip + rangeI'm open for suggestions on this message format.
Config changes
nftablesv6-cmda nftables command to run when a ipv6 packet is accepted.HOW was this pull request tested?
Specs? Manual steps? Please fill me in.