Skip to content
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
72562a7
Initial setup
codemob-dev Oct 30, 2025
21daaa2
Comp the ile
codemob-dev Oct 30, 2025
dc579d0
Memory leek
codemob-dev Oct 31, 2025
89db35d
The stupid is stupiding
codemob-dev Oct 31, 2025
4bce3bf
aaaa
codemob-dev Oct 31, 2025
e350bbd
I dids the stupids
codemob-dev Oct 31, 2025
b16d22e
I dids the even more stupids
codemob-dev Oct 31, 2025
c4d9f86
u32 in a pointer costume 🔥🔥
codemob-dev Oct 31, 2025
c073f08
Remove unnececarry error
codemob-dev Oct 31, 2025
255aaef
Progress on fixing things
codemob-dev Nov 2, 2025
d88f7de
Merge branch 'master' into ip-banning
codemob-dev Nov 2, 2025
5dc3931
Stupidstupidstupidstupid
codemob-dev Nov 2, 2025
c5e132b
Merge branch 'master' into ip-banning
codemob-dev Nov 7, 2025
aa67a3b
Progress towards fixing things
codemob-dev Nov 7, 2025
7e43926
Make the tests run
codemob-dev Nov 10, 2025
f1268b4
More tests
codemob-dev Nov 10, 2025
6c60dde
kick player
codemob-dev Nov 10, 2025
110eacd
Formatting
codemob-dev Nov 10, 2025
f7bbf5b
Remove ip banning stuff
codemob-dev Nov 11, 2025
bc423d5
Merge branch 'master' into address-struct
codemob-dev Nov 11, 2025
5d654a5
Finish reverting stuff
codemob-dev Nov 11, 2025
0ebc51c
Merge branch 'master' into address-struct
codemob-dev Nov 13, 2025
81f40ff
Better errors
codemob-dev Nov 13, 2025
dfaaecd
Refactor functions
codemob-dev Nov 13, 2025
db6b4de
Formatting
codemob-dev Nov 13, 2025
d132900
Merge branch 'master' into address-struct
codemob-dev Nov 18, 2025
7f815a2
Fix things (WIP)
codemob-dev Nov 18, 2025
63d0254
Redo a lot of stuff
codemob-dev Nov 18, 2025
862c865
redundant thing
codemob-dev Nov 18, 2025
b6371b4
Use the local allocator
codemob-dev Nov 18, 2025
b0bc4ff
remove unneeded thing
codemob-dev Nov 18, 2025
58c26dc
oops
codemob-dev Nov 18, 2025
acd636e
Remove accidental changes
codemob-dev Nov 18, 2025
1bc5f68
Formatting
codemob-dev Nov 18, 2025
dd28790
Add default port parameter
codemob-dev Nov 18, 2025
d678bd2
Fix resolve tests
codemob-dev Nov 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 134 additions & 59 deletions src/network.zig
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ const Socket = struct {
posix.close(self.socketID);
}

fn send(self: Socket, data: []const u8, destination: Address) void {
fn send(self: Socket, data: []const u8, destination: SocketAddress) void {
const addr = posix.sockaddr.in{
.port = @byteSwap(destination.port),
.addr = destination.ip,
.addr = destination.ip.address,
};
if(builtin.os.tag == .windows) { // TODO: Upstream error, fix after next Zig update after #24466 is merged
const sendto = struct {
Expand All @@ -83,7 +83,7 @@ const Socket = struct {
}
}

fn receive(self: Socket, buffer: []u8, timeout: i32, resultAddress: *Address) ![]u8 {
fn receive(self: Socket, buffer: []u8, timeout: i32, resultAddress: *SocketAddress) ![]u8 {
if(builtin.os.tag == .windows) { // Of course Windows always has it's own special thing.
var pfd = [1]posix.pollfd{
.{.fd = self.socketID, .events = std.c.POLL.RDNORM | std.c.POLL.RDBAND, .revents = undefined},
Expand Down Expand Up @@ -111,17 +111,11 @@ const Socket = struct {
var addr: posix.sockaddr.in = undefined;
var addrLen: posix.socklen_t = @sizeOf(posix.sockaddr.in);
const length = try posix.recvfrom(self.socketID, buffer, 0, @ptrCast(&addr), &addrLen);
resultAddress.ip = addr.addr;
resultAddress.ip = .{.address = addr.addr};
resultAddress.port = @byteSwap(addr.port);
return buffer[0..length];
}

fn resolveIP(addr: []const u8) !u32 {
const list = try std.net.getAddressList(main.stackAllocator.allocator, addr, settings.defaultPort);
defer list.deinit();
return list.addrs[0].in.sa.addr;
}

fn getPort(self: Socket) !u16 {
var addr: posix.sockaddr.in = undefined;
var addrLen: posix.socklen_t = @sizeOf(posix.sockaddr.in);
Expand All @@ -145,24 +139,84 @@ pub fn init() void {
}
}

pub const Address = struct {
ip: u32,
pub const AddressParseMode = enum {parse, resolve, parseOrResolve};

pub const IpAddress = struct {
address: u32,

pub const localHost = IpAddress{.address = 0x0100007f};

pub fn format(self: IpAddress, writer: anytype) !void {
try writer.print("{}.{}.{}.{}", .{self.address & 255, self.address >> 8 & 255, self.address >> 16 & 255, self.address >> 24});
}

pub fn fromString(addr: []const u8, comptime mode: AddressParseMode, allocator: NeverFailingAllocator) !IpAddress {
return switch(mode) {
.parse => return parse(addr),
.resolve => return resolve(allocator, addr, settings.defaultPort),
.parseOrResolve => return parse(addr) catch resolve(allocator, addr, settings.defaultPort),
};
}

fn resolve(allocator: NeverFailingAllocator, addr: []const u8, port: u16) !IpAddress {
const list = try std.net.getAddressList(allocator.allocator, addr, port);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the stackAllocator for local allocations

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't work in tests

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the error? It seems odd that it wouldn't work. Can't you test network code in Zig?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the error? It seems odd that it wouldn't work. Can't you test network code in Zig?

The stack allocator isn't available during tests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, then please fix #1286 in a separate PR first.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, then please fix #1286 in a separate PR first.

#2320

defer list.deinit();
return .{.address = list.addrs[0].in.sa.addr};
}

fn parse(addr: []const u8) !IpAddress {
var parts = std.mem.splitScalar(u8, addr, '.');
var address: u32 = 0;
while(parts.next()) |part| {
const octet = try std.fmt.parseInt(u8, part, 10);
address >>= 8;
if(address >> 24 > 0) return error.TooManyOctets;
address |= @as(u32, octet) << 24;
}
return .{.address = address};
}
};

pub const SocketAddress = struct {
ip: IpAddress,
port: u16,
isSymmetricNAT: bool = false,

pub const localHost = 0x0100007f;

pub fn format(self: Address, writer: anytype) !void {
pub fn format(self: SocketAddress, writer: anytype) !void {
if(self.isSymmetricNAT) {
try writer.print("{}.{}.{}.{}:?{}", .{self.ip & 255, self.ip >> 8 & 255, self.ip >> 16 & 255, self.ip >> 24, self.port});
try writer.print("{f}:?{}", .{self.ip, self.port});
} else {
try writer.print("{}.{}.{}.{}:{}", .{self.ip & 255, self.ip >> 8 & 255, self.ip >> 16 & 255, self.ip >> 24, self.port});
try writer.print("{f}:{}", .{self.ip, self.port});
}
}

/// The allocator can be left undefined when the mode is `.parse`.
pub fn fromString(string: []const u8, comptime mode: AddressParseMode, allocator: NeverFailingAllocator) !SocketAddress {
var parts = std.mem.splitScalar(u8, string, ':');
const ipString = parts.next() orelse unreachable;
var portString = parts.next() orelse return error.MissingColon;
if(parts.next() != null) return error.MultipleColons;
var isSymmetricNAT = false;
if(portString.len == 0) return error.EmptyPort;
if(portString[0] == '?') {
isSymmetricNAT = true;
portString = portString[1..];
}
const port = try std.fmt.parseInt(u16, portString, 10);
return .{
.ip = switch(mode) {
.parse => try IpAddress.parse(ipString),
.resolve => try IpAddress.resolve(allocator, ipString, port),
.parseOrResolve => IpAddress.parse(ipString) catch try IpAddress.resolve(allocator, ipString, port),
},
.isSymmetricNAT = isSymmetricNAT,
.port = port,
};
}
};

const Request = struct {
address: Address,
address: SocketAddress,
data: []const u8,
requestNotifier: std.Thread.Condition = std.Thread.Condition{},
};
Expand Down Expand Up @@ -272,8 +326,8 @@ const STUN = struct { // MARK: STUN
const XOR_MAPPED_ADDRESS: u16 = 0x0020;
const MAGIC_COOKIE = [_]u8{0x21, 0x12, 0xA4, 0x42};

fn requestAddress(connection: *ConnectionManager) Address {
var oldAddress: ?Address = null;
fn requestAddress(connection: *ConnectionManager) SocketAddress {
var oldAddress: ?SocketAddress = null;
var seed: [std.Random.DefaultCsprng.secret_seed_length]u8 = @splat(0);
std.mem.writeInt(i128, seed[0..16], std.time.nanoTimestamp(), builtin.cpu.arch.endian()); // Not the best seed, but it's not that important.
var random = std.Random.DefaultCsprng.init(seed);
Expand All @@ -288,14 +342,9 @@ const STUN = struct { // MARK: STUN
};
random.fill(data[8..]); // Fill the transaction ID.

var splitter = std.mem.splitScalar(u8, server, ':');
const ip = splitter.first();
const serverAddress = Address{
.ip = Socket.resolveIP(ip) catch |err| {
std.log.warn("Cannot resolve stun server address: {s}, error: {s}", .{ip, @errorName(err)});
continue;
},
.port = std.fmt.parseUnsigned(u16, splitter.rest(), 10) catch 3478,
const serverAddress = SocketAddress.fromString(server, .resolve, main.stackAllocator) catch |err| {
std.log.warn("Cannot resolve stun server address: {s}, error: {s}", .{server, @errorName(err)});
continue;
};
if(connection.sendRequest(main.globalAllocator, &data, serverAddress, 500*1000000)) |answer| {
defer main.globalAllocator.free(answer);
Expand All @@ -309,7 +358,7 @@ const STUN = struct { // MARK: STUN
};
if(oldAddress) |other| {
std.log.info("{f}", .{result});
if(other.ip == result.ip and other.port == result.port) {
if(other.ip.address == result.ip.address and other.port == result.port) {
return result;
} else {
result.isSymmetricNAT = true;
Expand All @@ -322,10 +371,10 @@ const STUN = struct { // MARK: STUN
std.log.warn("Couldn't reach STUN server: {s}", .{server});
}
}
return Address{.ip = Socket.resolveIP("127.0.0.1") catch unreachable, .port = settings.defaultPort}; // TODO: Return ip address in LAN.
return SocketAddress{.ip = IpAddress.localHost, .port = settings.defaultPort}; // TODO: Return ip address in LAN.
}

fn findIPPort(_data: []const u8) !Address {
fn findIPPort(_data: []const u8) !SocketAddress {
var data = _data[20..]; // Skip the header.
while(data.len > 0) {
const typ = std.mem.readInt(u16, data[0..2], .big);
Expand All @@ -345,9 +394,9 @@ const STUN = struct { // MARK: STUN
addressData[4] ^= MAGIC_COOKIE[2];
addressData[5] ^= MAGIC_COOKIE[3];
}
return Address{
return SocketAddress{
.port = std.mem.readInt(u16, addressData[0..2], .big),
.ip = std.mem.readInt(u32, addressData[2..6], builtin.cpu.arch.endian()), // Needs to stay in big endian → native.
.ip = .{.address = std.mem.readInt(u32, addressData[2..6], builtin.cpu.arch.endian())}, // Needs to stay in big endian → native.
};
} else if(data[1] == 0x02) {
data = data[(len + 3) & ~@as(usize, 3) ..]; // Pad to 32 Bit.
Expand Down Expand Up @@ -380,7 +429,7 @@ pub const ConnectionManager = struct { // MARK: ConnectionManager
socket: Socket = undefined,
thread: std.Thread = undefined,
threadId: std.Thread.Id = undefined,
externalAddress: Address = undefined,
externalAddress: SocketAddress = undefined,
online: Atomic(bool) = .init(false),
running: Atomic(bool) = .init(true),

Expand All @@ -401,7 +450,7 @@ pub const ConnectionManager = struct { // MARK: ConnectionManager

const PacketSendRequest = struct {
data: []const u8,
target: Address,
target: SocketAddress,
time: i64,

fn compare(_: void, a: PacketSendRequest, b: PacketSendRequest) std.math.Order {
Expand Down Expand Up @@ -467,7 +516,7 @@ pub const ConnectionManager = struct { // MARK: ConnectionManager
}
}

pub fn send(self: *ConnectionManager, data: []const u8, target: Address, nanoTime: ?i64) void {
pub fn send(self: *ConnectionManager, data: []const u8, target: SocketAddress, nanoTime: ?i64) void {
if(nanoTime) |time| {
self.mutex.lock();
defer self.mutex.unlock();
Expand All @@ -481,7 +530,7 @@ pub const ConnectionManager = struct { // MARK: ConnectionManager
}
}

pub fn sendRequest(self: *ConnectionManager, allocator: NeverFailingAllocator, data: []const u8, target: Address, timeout_ns: u64) ?[]const u8 {
pub fn sendRequest(self: *ConnectionManager, allocator: NeverFailingAllocator, data: []const u8, target: SocketAddress, timeout_ns: u64) ?[]const u8 {
self.socket.send(data, target);
var request = Request{.address = target, .data = data};
{
Expand Down Expand Up @@ -517,7 +566,7 @@ pub const ConnectionManager = struct { // MARK: ConnectionManager
self.mutex.lock();
defer self.mutex.unlock();
for(self.connections.items) |other| {
if(other.remoteAddress.ip == conn.remoteAddress.ip and other.remoteAddress.port == conn.remoteAddress.port) return error.AlreadyConnected;
if(other.remoteAddress.ip.address == conn.remoteAddress.ip.address and other.remoteAddress.port == conn.remoteAddress.port) return error.AlreadyConnected;
}
self.connections.append(conn);
}
Expand All @@ -541,12 +590,12 @@ pub const ConnectionManager = struct { // MARK: ConnectionManager
}
}

fn onReceive(self: *ConnectionManager, data: []const u8, source: Address) void {
fn onReceive(self: *ConnectionManager, data: []const u8, source: SocketAddress) void {
std.debug.assert(self.threadId == std.Thread.getCurrentId());
self.mutex.lock();

for(self.connections.items) |conn| {
if(conn.remoteAddress.ip == source.ip) {
if(conn.remoteAddress.ip.address == source.ip.address) {
if(conn.bruteforcingPort) {
conn.remoteAddress.port = source.port;
conn.bruteforcingPort = false;
Expand All @@ -562,15 +611,15 @@ pub const ConnectionManager = struct { // MARK: ConnectionManager
defer self.mutex.unlock();
// Check if it's part of an active request:
for(self.requests.items) |request| {
if(request.address.ip == source.ip and request.address.port == source.port) {
if(request.address.ip.address == source.ip.address and request.address.port == source.port) {
request.data = main.globalAllocator.dupe(u8, data);
request.requestNotifier.signal();
return;
}
}
if(self.online.load(.acquire) and source.ip == self.externalAddress.ip and source.port == self.externalAddress.port) return;
if(self.online.load(.acquire) and source.ip.address == self.externalAddress.ip.address and source.port == self.externalAddress.port) return;
}
if(self.allowNewConnections.load(.monotonic) or source.ip == Address.localHost) {
if(self.allowNewConnections.load(.monotonic) or source.ip.address == IpAddress.localHost.address) {
if(data.len != 0 and data[0] == @intFromEnum(Connection.ChannelId.init)) {
const ip = std.fmt.allocPrint(main.stackAllocator.allocator, "{f}", .{source}) catch unreachable;
defer main.stackAllocator.free(ip);
Expand All @@ -596,7 +645,7 @@ pub const ConnectionManager = struct { // MARK: ConnectionManager
while(self.running.load(.monotonic)) {
main.heap.GarbageCollection.syncPoint();
self.waitingToFinishReceive.broadcast();
var source: Address = undefined;
var source: SocketAddress = undefined;
if(self.socket.receive(&self.receiveBuffer, 1, &source)) |data| {
self.onReceive(data, source);
} else |err| {
Expand Down Expand Up @@ -1894,7 +1943,7 @@ pub const Connection = struct { // MARK: Connection
manager: *ConnectionManager,
user: ?*main.server.User,

remoteAddress: Address,
remoteAddress: SocketAddress,
bruteforcingPort: bool = false,
bruteForcedPortRange: u16 = 0,

Expand Down Expand Up @@ -1953,20 +2002,8 @@ pub const Connection = struct { // MARK: Connection
result.queuedConfirmations.deinit();
}
if(result.connectionIdentifier == 0) result.connectionIdentifier = 1;

var splitter = std.mem.splitScalar(u8, ipPort, ':');
const ip = splitter.first();
result.remoteAddress.ip = try Socket.resolveIP(ip);
var port = splitter.rest();
if(port.len != 0 and port[0] == '?') {
result.remoteAddress.isSymmetricNAT = true;
result.bruteforcingPort = true;
port = port[1..];
}
result.remoteAddress.port = std.fmt.parseUnsigned(u16, port, 10) catch blk: {
if(ip.len != ipPort.len) std.log.err("Could not parse port \"{s}\". Using default port instead.", .{port});
break :blk settings.defaultPort;
};
result.remoteAddress = try SocketAddress.fromString(ipPort, .parseOrResolve, main.stackAllocator);
result.bruteforcingPort = result.remoteAddress.isSymmetricNAT;

try result.manager.addConnection(result);
return result;
Expand Down Expand Up @@ -2345,3 +2382,41 @@ const ProtocolTask = struct {
main.globalAllocator.destroy(self);
}
};

test "Parse address" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also add a test for resolve

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would I do that? Domain names can change IPs, should I just test to make sure that the parsing functions match the resolve functions for IPs?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not all of them. localhost is one such example

Copy link
Contributor Author

@codemob-dev codemob-dev Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

localhost is one such example

Depending on the computer's config it might resolve differently.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I could edit it in /etc/hosts if I wanted to, but is that something we should worry about?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should include some of those undead services, like www.google.com

If you are not connected to the internet, then any service apart from localhost is dead.

Yeah it would be great to test if Cubyz works w/o internet connection right?

But most of the time we care about name resolution in the context of working with an internet connection, so I am proposing positive test cases first.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to run all test cases without an internet connection.
That is a hard requirement for me, since it makes development more accessible (e.g. I can test code when I'm on the train, without having to figure out how to setup their free wifi).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test cases should just mark themselves as skipped if they can't be runner in current environment. With your requirement you will never be able to test anything platform specific, as you can't be running both windows and Linux as host os at once. Test cases may not be ran every time, not all at once, but they have to be checked at least in CI. It's normal, it's everywhere, let's not reinvent the wheel.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be good, yes. But I would still rather avoid adding another dependency on a random service. We haven't used google for anything, so why add a dependency on it for some stupid test case?
The only online dependency we have is github at the moment, but to be honest I do not think we should increase or dependency on github for a stupid test case.
Furthermore the thing we are testing is probably something that should have been tested in the Zig repo, and not in our code. What we care about is that the hostname is forwarded to the right function, not that the function produces the right results for some arbitrary hostname. For that "localhost" is enough.

Copy link
Member

@IntegratedQuantum IntegratedQuantum Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plus any online connection increases the chance sporadic CI failures.
We have already seen this for the existing online dependencies (see https://discord.com/channels/443805812390100992/485498631156138005/1433920016318988398)

const address = try IpAddress.fromString("127.0.0.1", .parse, undefined);

try std.testing.expectEqual(IpAddress.localHost, address);
const socketAddress = try SocketAddress.fromString("127.0.0.1:1234", .parse, undefined);
try std.testing.expectEqual(SocketAddress{.ip = IpAddress.localHost, .port = 1234}, socketAddress);

const symmetricSocketAddress = try SocketAddress.fromString("127.0.0.1:?1234", .parse, undefined);
try std.testing.expectEqual(SocketAddress{.ip = IpAddress.localHost, .isSymmetricNAT = true, .port = 1234}, symmetricSocketAddress);
}

test "Format address" {
const addresses: [4][]const u8 = .{
"127.0.0.1",
"123.1.111.222",
"1.1.1.1",
"0.0.0.0",
};
for(addresses) |addressStr| {
const address = try IpAddress.fromString(addressStr, .parse, undefined);
const reformattedAddress = std.fmt.allocPrint(main.heap.testingAllocator.allocator, "{f}", .{address}) catch unreachable;
defer main.heap.testingAllocator.free(reformattedAddress);
try std.testing.expectEqualStrings(addressStr, reformattedAddress);
}
const socketAddresses: [4][]const u8 = .{
"127.0.0.1:1234",
"123.1.111.222:?11111",
"1.1.1.1:255",
"0.0.0.0:?3333",
};
for(socketAddresses) |addressStr| {
const address = try SocketAddress.fromString(addressStr, .parse, undefined);
const reformattedAddress = std.fmt.allocPrint(main.heap.testingAllocator.allocator, "{f}", .{address}) catch unreachable;
defer main.heap.testingAllocator.free(reformattedAddress);
try std.testing.expectEqualStrings(addressStr, reformattedAddress);
}
}
Loading