Skip to content
Merged
Changes from 52 commits
Commits
Show all changes
55 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
757969f
Merge branch 'master' into address-struct
codemob-dev May 4, 2026
ae2cdf1
Remove duplicate ProtocolTask struct from merge
codemob-dev May 4, 2026
76f6933
Fix some accidental removed code from merging
codemob-dev May 4, 2026
71b9faa
Fix parseInner function
codemob-dev May 4, 2026
664f668
Use Socket.resolveIP
codemob-dev May 4, 2026
6e0ab76
It compiles, yay!
codemob-dev May 4, 2026
c0e8ab7
Improve the tests
codemob-dev May 4, 2026
cfa5c08
Formatting
codemob-dev May 4, 2026
2933138
use Ip4Address for inner
Wunka May 7, 2026
99a5b3d
use addressToPosix at some places to prepare for ip6 enabelment
Wunka May 8, 2026
76c15c1
type
Wunka May 8, 2026
bbb22f1
use generalized IpAddress instead of Ip4Address
Wunka May 8, 2026
e340562
fix tests
Wunka May 8, 2026
08bafad
Merge branch 'master' into innerIpAddress
Wunka Jul 6, 2026
d823a2a
remove parse as quantum requested from codemob
Wunka Jul 19, 2026
1175401
remove faulty tests
Wunka Jul 19, 2026
9027d9e
fix
Wunka Jul 25, 2026
12ccadd
fix 2
Wunka Jul 26, 2026
d4fbcfe
Merge remote-tracking branch 'upstream/master' into innerIpAddress
Wunka Aug 1, 2026
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
200 changes: 137 additions & 63 deletions src/network.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const builtin = @import("builtin");
const std = @import("std");
const Atomic = std.atomic.Value;
const IpAddress = std.Io.net.IpAddress;

const main = @import("main");
const game = main.game;
Expand Down Expand Up @@ -115,10 +116,11 @@ const Socket = struct {
}
}

fn send(self: Socket, data: []const u8, destination: Address) void {
const addr = posix.sockaddr.in{
.port = @byteSwap(destination.port),
.addr = destination.ip,
fn send(self: Socket, data: []const u8, destination: SocketAddress) void {
const addr = blk: {
var posixAddr: std.Io.Threaded.PosixAddress = undefined;
_ = std.Io.Threaded.addressToPosix(&destination.address, &posixAddr);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The length (return value) should probably be passed into the other functions instead of using @sizeOf

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

both done

break :blk posixAddr.in;
};
if (builtin.os.tag == .windows) {
const result = c.sendto(self.socketID, data.ptr, @intCast(data.len), 0, @ptrCast(&addr), @sizeOf(posix.sockaddr.in));
Expand All @@ -141,7 +143,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]c.pollfd{
.{.fd = self.socketID, .events = c.POLLRDNORM | c.POLLRDBAND, .revents = undefined},
Expand Down Expand Up @@ -181,22 +183,21 @@ const Socket = struct {
}
}
};
resultAddress.ip = addr.addr;
resultAddress.port = @byteSwap(addr.port);
resultAddress.address = std.Io.Threaded.addressFromPosix(&std.Io.Threaded.PosixAddress{.in = addr});
return buffer[0..length];
}

fn resolveIP(name: []const u8) !u32 {
fn resolveIP(name: []const u8, port: u16) !IpAddress {
var nameBuf: [255]u8 = undefined;
var buf: [16]std.Io.net.HostName.LookupResult = undefined;
var resultQueue = std.Io.Queue(std.Io.net.HostName.LookupResult).init(&buf);
try std.Io.net.HostName.lookup(try .init(name), main.io, &resultQueue, .{.canonical_name_buffer = &nameBuf, .port = 0});
try std.Io.net.HostName.lookup(try .init(name), main.io, &resultQueue, .{.canonical_name_buffer = &nameBuf, .port = port});
while (true) {
const entry = resultQueue.getOneUncancelable(main.io) catch break;
switch (entry) {
.address => |addr| {
if (addr != .ip4) continue;
return std.mem.bytesToValue(u32, addr.ip4.bytes[0..4]);
return addr;
},
.canonical_name => {},
}
Expand Down Expand Up @@ -237,24 +238,64 @@ pub fn deinit() void {
c.mbedtls_psa_crypto_free();
}

pub const Address = struct {
ip: u32,
port: u16,
pub const SocketAddress = struct {
address: IpAddress,
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.address});
} 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.address});
}
}

fn parseInner(string: []const u8, defaultPort: ?u16) !struct { ip: []const u8, isSymmetricNAT: bool, port: u16 } {
var parts = std.mem.splitScalar(u8, string, ':');
const ip = parts.first();
var portString = parts.rest();
var isSymmetricNAT = false;
if (portString.len > 0 and portString[portString.len - 1] == '?') {
isSymmetricNAT = true;
portString = portString[0 .. portString.len - 1];
}
const port = try (std.fmt.parseUnsigned(u16, portString, 10) catch |err| (defaultPort orelse err));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Instead of ignoring the error when a defaultPort is present, this should only use the default port when the portString.len == 0

return .{
.ip = ip,
.isSymmetricNAT = isSymmetricNAT,
.port = port,
};
}

pub fn resolve(string: []const u8, defaultPort: ?u16) !SocketAddress {
const innerResult = try parseInner(string, defaultPort);
return .{
.address = try Socket.resolveIP(innerResult.ip, innerResult.port),
.isSymmetricNAT = innerResult.isSymmetricNAT,
};
}

// copied from zig std, just without the port == port
pub fn eqlInnerWithoutPort(self: *const SocketAddress, other: *const SocketAddress) bool {
return switch (self.address) {
.ip4 => |a_ip4| switch (other.address) {
.ip4 => |b_ip4| {
const a_int: u32 = @bitCast(a_ip4.bytes);
const b_int: u32 = @bitCast(b_ip4.bytes);
return a_int == b_int;
},
else => false,
},
.ip6 => |a_ip6| switch (other.address) {
.ip6 => |b_ip6| std.mem.eql(u8, &a_ip6.bytes, &b_ip6.bytes),
else => false,
},
};
}
};

const Request = struct {
address: Address,
address: SocketAddress,
data: []const u8,
requestNotifier: main.utils.Condition = .{},
};
Expand Down Expand Up @@ -364,8 +405,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], main.timestamp().toMilliseconds(), builtin.cpu.arch.endian()); // Not the best seed, but it's not that important.
var random = std.Random.DefaultCsprng.init(seed);
Expand All @@ -380,14 +421,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.resolve(server, null) catch |err| {
std.log.warn("Cannot resolve STUN server address: {s}, error: {s}", .{server, @errorName(err)});
continue;
};
if (connection.sendRequest(main.globalAllocator, &data, serverAddress, .fromMilliseconds(500))) |answer| {
defer main.globalAllocator.free(answer);
Expand All @@ -400,7 +436,7 @@ const stun = struct { // MARK: stun
continue;
};
if (oldAddress) |other| {
if (other.ip == result.ip and other.port == result.port) {
if (other.address.eql(&result.address)) {
return result;
} else {
std.log.warn("Detected symmetric NAT. UDP-holepunching may not work reliably", .{});
Expand All @@ -414,10 +450,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{.address = .{.ip4 = .loopback(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 @@ -437,9 +473,13 @@ const stun = struct { // MARK: stun
addressData[4] ^= MAGIC_COOKIE[2];
addressData[5] ^= MAGIC_COOKIE[3];
}
return Address{
.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.
return SocketAddress{
.address = .{
.ip4 = .{
.port = std.mem.readInt(u16, addressData[0..2], .big),
.bytes = std.mem.toBytes(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 @@ -472,7 +512,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(false),

Expand All @@ -493,7 +533,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 @@ -562,7 +602,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 @@ -576,7 +616,7 @@ pub const ConnectionManager = struct { // MARK: ConnectionManager
}
}

pub fn sendRequest(self: *ConnectionManager, allocator: NeverFailingAllocator, data: []const u8, target: Address, timeout: std.Io.Duration) ?[]const u8 {
pub fn sendRequest(self: *ConnectionManager, allocator: NeverFailingAllocator, data: []const u8, target: SocketAddress, timeout: std.Io.Duration) ?[]const u8 {
self.socket.send(data, target);
var request = Request{.address = target, .data = data};
{
Expand Down Expand Up @@ -612,7 +652,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.address.eql(&conn.remoteAddress.address)) return error.AlreadyConnected;
}
self.connections.append(main.globalAllocator, conn);
}
Expand All @@ -636,17 +676,17 @@ 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.eqlInnerWithoutPort(&source)) {
if (conn.bruteforcingPort) {
conn.remoteAddress.port = source.port;
conn.remoteAddress.address.setPort(source.address.getPort());
conn.bruteforcingPort = false;
}
if (conn.remoteAddress.port == source.port) {
if (conn.remoteAddress.address.getPort() == source.address.getPort()) {
self.mutex.unlock();
conn.receive(data);
return;
Expand All @@ -657,15 +697,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.address.eql(&source.address)) {
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.address.eql(&self.externalAddress.address)) return;
}
if (self.allowNewConnections or source.ip == Address.localHost) {
if (self.allowNewConnections or source.address.eql(&IpAddress{.ip4 = .loopback(source.address.getPort())})) {
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 Down Expand Up @@ -693,7 +733,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 @@ -1433,7 +1473,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 @@ -1493,20 +1533,8 @@ pub const Connection = struct { // MARK: Connection
try result.secureChannel.init(main.random.nextInt(SequenceIndex, &main.seed), 10*ms, .secure, if (user != null) .server else .client);
errdefer result.secureChannel.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.resolve(ipPort, settings.defaultPort);
result.bruteforcingPort = result.remoteAddress.isSymmetricNAT;

try result.manager.addConnection(result);
return result;
Expand Down Expand Up @@ -1850,3 +1878,49 @@ pub const Connection = struct { // MARK: Connection
std.log.info("Disconnected", .{});
}
};

test "Resolve 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 parsedAddress = try IpAddress.parse(addressStr, 0);
const resolvedAddress = try Socket.resolveIP(addressStr, 0);
try std.testing.expectEqualDeep(parsedAddress, resolvedAddress);
}
const resolvedLocalhost = try Socket.resolveIP("localhost", 0);
try std.testing.expectEqualDeep(IpAddress{.ip4 = .loopback(0)}, resolvedLocalhost);

const localhostSocketAddresses: [3][]const u8 = .{
"localhost",
"localhost:1234",
"localhost:11111?",
};
const expectedLocalhostSocketAddresses: [3]SocketAddress = .{
.{.address = .{.ip4 = .loopback(888)}},
.{.address = .{.ip4 = .loopback(1234)}},
.{.address = .{.ip4 = .loopback(11111)}, .isSymmetricNAT = true},
};
for (localhostSocketAddresses, expectedLocalhostSocketAddresses) |addressStr, expected| {
const resolvedAddress = try SocketAddress.resolve(addressStr, 888);
try std.testing.expectEqualDeep(expected, resolvedAddress);
}
}

test "Format address" {
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.resolve(addressStr, null);
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