Accommodate IPv6 addresses at packet level. Avoid infinite loops in deserialisation. Test with timing loop removed.

This commit is contained in:
wan-may 2023-09-26 00:05:54 -03:00
parent 74e328d86c
commit d011885c87
11 changed files with 108 additions and 64 deletions

View File

@ -19,4 +19,5 @@ return setmetatable({
["svinfo_port"] = "Port", ["svinfo_port"] = "Port",
["refresh_button"] = "Refresh", ["refresh_button"] = "Refresh",
["cancel_button"] = "Cancel", ["cancel_button"] = "Cancel",
game_intro = "A gaze blank and pitiless as the sun",
}, {__index = function( t, k ) return k end } ) }, {__index = function( t, k ) return k end } )

View File

@ -6,22 +6,44 @@ local packet = shared.packet
local crepuscular = assert( require 'crepuscular' ) local crepuscular = assert( require 'crepuscular' )
local game = {} local game = {}
local handlers = setmetatable( {}, {__index = function() return print end } )
local t = 0 local t = 0
local tick = 0 local tick = 0
local serverTick = "0" local serverTick = 0
function handlers.connected( data )
serverTick = math.max( data.tick, serverTick )
end
function handlers.insect( data )
end
function handlers.soleil( data )
end
function handlers.playerChange( data )
end
function handlers.chatMessage( msg )
print( msg.cmsg )
end
function game.draw() function game.draw()
lg.setColor( 1, 1, 1, 1 )
lg.print( tick, 0, 0 ) lg.print( tick, 0, 0 )
lg.print( serverTick, 0, 25 ) lg.print( serverTick, 0, 25 )
end end
function game.onPacket( msg ) function game.onPacket( msg )
if not msg then return end if not msg or (#msg < 1) then return end
local msgs, types = packet.deserialise( msg ) local msgs, types = packet.deserialise( msg )
if not msgs then return game.onPacket( server.receive() ) end if not msgs then return game.onPacket( server.receive() ) end
for i = 1, #msgs do for i = 1, #msgs do
game[ types[i] ]( msgs[i], ip, port ) --Handler returns something if msg should be discarded.
if handlers[ types[i] ]( msgs[i] ) then break end
end end
return game.onPacket( server.receive() ) return game.onPacket( server.receive() )
end end
@ -32,7 +54,8 @@ function game.update( dt )
if t > 0.1 then if t > 0.1 then
t = 0 t = 0
tick = tick + 1 tick = tick + 1
assert( server.send( packet.get( packet.heartbeat{ tick = tick, hash = 1234 } ) ) ) server.newPacket()
assert( server.send( packet.get() ) )
end end
end end

View File

@ -31,6 +31,11 @@ function udp.requestServerList()
return mscxn:send( packet.get() ) return mscxn:send( packet.get() )
end end
function udp.newPacket( tick )
packet.get()
if token then packet.connected{ token = token, tick = tick or 0 } end
end
function udp.setToken( token ) function udp.setToken( token )
token = token token = token
end end
@ -51,6 +56,7 @@ end
function udp.connect( ip, port ) function udp.connect( ip, port )
assert( cxn:setpeername( ip, port ) ) assert( cxn:setpeername( ip, port ) )
print( "Connection request to:", ip, port )
return udp.send( packet.get( packet.clientInfo{ username = config.plName } ) ) return udp.send( packet.get( packet.clientInfo{ username = config.plName } ) )
end end
@ -61,7 +67,6 @@ function udp.disconnect( )
end end
function udp.send( s ) function udp.send( s )
print( "udp out:", s )
return cxn:send( s ) return cxn:send( s )
end end

View File

@ -210,6 +210,8 @@ end
function browser.joinIPString( s ) function browser.joinIPString( s )
--Parse IP address and port from string. If it's valid, join the server. --Parse IP address and port from string. If it's valid, join the server.
--TODO: there should be two fields, one for IP, one for port.
--Parsing the entered address for the port is possible but more error-prone.
print( "browser: entered IP and port", s ) print( "browser: entered IP and port", s )
if not s then return end if not s then return end
ti:clear() ti:clear()

View File

@ -34,18 +34,18 @@ function love.conf(t)
t.modules.data = true -- Enable the data module (boolean) t.modules.data = true -- Enable the data module (boolean)
t.modules.event = true -- Enable the event module (boolean) t.modules.event = true -- Enable the event module (boolean)
t.modules.font = false -- Enable the font module (boolean) t.modules.font = false -- Enable the font module (boolean)
t.modules.graphics = true -- Enable the graphics module (boolean) t.modules.graphics = false -- Enable the graphics module (boolean)
t.modules.image = true -- Enable the image module (boolean) t.modules.image = false -- Enable the image module (boolean)
t.modules.joystick = false -- Enable the joystick module (boolean) t.modules.joystick = false -- Enable the joystick module (boolean)
t.modules.keyboard = true -- Enable the keyboard module (boolean) t.modules.keyboard = false -- Enable the keyboard module (boolean)
t.modules.math = true -- Enable the math module (boolean) t.modules.math = false -- Enable the math module (boolean)
t.modules.mouse = true -- Enable the mouse module (boolean) t.modules.mouse = false -- Enable the mouse module (boolean)
t.modules.physics = false -- Enable the physics module (boolean) t.modules.physics = false -- Enable the physics module (boolean)
t.modules.sound = false -- Enable the sound module (boolean) t.modules.sound = false -- Enable the sound module (boolean)
t.modules.system = true -- Enable the system module (boolean) t.modules.system = false -- Enable the system module (boolean)
t.modules.thread = true -- Enable the thread module (boolean) t.modules.thread = false -- Enable the thread module (boolean)
t.modules.timer = true -- Enable the timer module (boolean), Disabling it will result 0 delta time in love.update t.modules.timer = true -- Enable the timer module (boolean), Disabling it will result 0 delta time in love.update
t.modules.touch = false -- Enable the touch module (boolean) t.modules.touch = false -- Enable the touch module (boolean)
t.modules.video = false -- Enable the video module (boolean) t.modules.video = false -- Enable the video module (boolean)
t.modules.window = true -- Enable the window module (boolean) t.modules.window = false -- Enable the window module (boolean)
end end

View File

@ -29,7 +29,7 @@ local handlers = setmetatable({
--NAT punch: the server doesn't know its own external IP --NAT punch: the server doesn't know its own external IP
--so it contacts a third party (the metaserver) to discover it. --so it contacts a third party (the metaserver) to discover it.
--This external IP gets advertised to prospective clients. --This external IP gets advertised to prospective clients.
svInfo.ip = shared.ip.fromString( ip ) svInfo.ip.ip = ip
svInfo.port = port svInfo.port = port
end end
servers[ip..port].time = t servers[ip..port].time = t
@ -56,8 +56,7 @@ local handlers = setmetatable({
local t = socket.gettime() local t = socket.gettime()
clients[ip].time = t clients[ip].time = t
packet.heartbeat{ tick = tick }
for svIP, server in pairs( servers ) do for svIP, server in pairs( servers ) do
print( "", svIP, packet.getString( server.info.svname )) print( "", svIP, packet.getString( server.info.svname ))
packet.serverInfo( server.info ) packet.serverInfo( server.info )

View File

@ -47,5 +47,5 @@ function love.conf(t)
t.modules.timer = true -- Enable the timer module (boolean), Disabling it will result 0 delta time in love.update t.modules.timer = true -- Enable the timer module (boolean), Disabling it will result 0 delta time in love.update
t.modules.touch = false -- Enable the touch module (boolean) t.modules.touch = false -- Enable the touch module (boolean)
t.modules.video = false -- Enable the video module (boolean) t.modules.video = false -- Enable the video module (boolean)
t.modules.window = true -- Enable the window module (boolean) t.modules.window = false -- Enable the window module (boolean)
end end

View File

@ -24,27 +24,48 @@ local server = { tick = 0, }
local handlers = setmetatable({ local handlers = setmetatable({
connected = function( msg, ip, port )
local client = clients[msg.token]
if not client or
(client.ip ~= ip) or
(client.port ~= port)
then
print( "Invalid token from IP:", msg.token, ip, port )
return true
end
if client.tick > msg.tick then
print( "Old packet received:", msg.tick, ip )
return true
end
client.tick = msg.tick
end,
clChimo = function( clChimo, ip, port ) clChimo = function( clChimo, ip, port )
if not connecting[ ip..port ] then local key = ip..":"..port
return print( "Old connection attempt from:", ip, port ) if not connecting[ key ] then
print( "Old connection attempt from:", key )
return true
end end
local remoteHash = clChimo.hash local remoteHash = clChimo.hash
local clNonce = clChimo.nonce local clNonce = clChimo.nonce
local svNonce = connecting[ip..port].nonce local svNonce = connecting[key].nonce
local localHash = shared.hash.hash( clNonce, svNonce ) local token = shared.hash.hash( clNonce, svNonce )
if localHash ~= remoteHash then if token ~= remoteHash then
return print( "Hashes differ:", shared.hash.hex( clNonce ), shared.hash.hex( svNonce ) ) print( "Hashes differ:", shared.hash.hex( clNonce ), shared.hash.hex( svNonce ) )
return true
end end
print( "Client connected:", ip, port, localHash ) print( "Client connected:", ip, port, token )
clients[ localHash ] = { ip = ip, port = port } clients[ token ] = connecting[ key ]
packet.connected{ token = localHash } packet.connected{ token = token, tick = server.tick }
return udp:sendto( packet.get(), ip, port ) return udp:sendto( packet.get(), ip, port )
end, end,
advertised = function( ack, ip, port ) advertised = function( ack, ip, port )
if ip ~= shared.metaserver.ip then return print( "Advertisement acked from rogue address:", ip, port ) end if ip ~= shared.metaserver.ip then return print( "Advertisement acked from rogue address:", ip, port ) end
if udp:getsockname() then if udp:getsockname() then
assert( udp:getsockname() == tostring( ack.ip ) ) --assert( udp:getsockname() == tostring( ack.ip ), print( tostring( ack.ip ), udp:getsockname()) or "IP mismatch!" )
return print( "Advertised. Address already set." ) return print( "Advertised. Address already set." )
end end
print( "Advertised. Setting address:", ack.ip, ack.port ) print( "Advertised. Setting address:", ack.ip, ack.port )
@ -53,8 +74,8 @@ local handlers = setmetatable({
clientInfo = function( clientInfo, ip, port ) clientInfo = function( clientInfo, ip, port )
local key = ip..port local key = ip..":"..port
connecting[key] = connecting[key] or {} connecting[key] = connecting[key] or { ip = ip, port = port, tick = 0 }
local client = connecting[key] local client = connecting[key]
local nonce = shared.hash.rand() local nonce = shared.hash.rand()
client.nonce = nonce client.nonce = nonce
@ -85,17 +106,16 @@ end
--Incoming packet. --Incoming packet.
function server.Parse( msg, ip, port ) function server.Parse( msg, ip, port )
if not msg then return end if (not msg) or (#msg < 1) then return end
if not clients[ip] then print( "Parsing:", ip, port, #msg, msg )
print( "New IP:", ip, port )
clients[ip] = { ip = ip, port = port }
end
local msgs, types = packet.deserialise( msg ) local msgs, types = packet.deserialise( msg )
if msgs then for i = 1, #msgs do if msgs then
print( "Received: ", types[i], ip, port ) for i = 1, #msgs do
handlers[ types[i] ]( msgs[i], ip, port ) print( "Received: ", types[i], ip, port )
end end if handlers[ types[i] ]( msgs[i], ip, port ) then break end
end
else print( types )
end
return server.Parse( udp:receivefrom() ) -- Process other packets. return server.Parse( udp:receivefrom() ) -- Process other packets.
end end
@ -124,8 +144,13 @@ end
function server.Advance() function server.Advance()
server.tick = server.tick + 1 server.tick = server.tick + 1
for id, client in pairs( clients ) do if server.tick % 100 == 0 then
-- for id, client in pairs( clients ) do
packet.get()
print( "updating client:", id )
packet.connected{ token = id, tick = server.tick }
udp:sendto( packet.get(), client.ip, client.port )
end
end end
end end
@ -142,7 +167,7 @@ server.Start()
function love.update( dt ) function love.update( dt )
server.Parse( udp:receivefrom() ) server.Parse( udp:receivefrom() )
server.Advance() server.Advance()
if server.tick % 250 == 0 then if server.tick % 4000 == 0 then
server.Advertise() server.Advertise()
server.Parse( mscxn:receive(), shared.metaserver.ip, shared.metaserver.port ) server.Parse( mscxn:receive(), shared.metaserver.ip, shared.metaserver.port )
end end

View File

@ -1,6 +1,6 @@
local math = math local math = math
local bit = assert( require 'bit' ) local bit = assert( require 'bit' )
local max = bit.tobit( 0xffffffff ) local max = math.huge
math.randomseed( 4 ) math.randomseed( 4 )
--hash of a pair of 32-bit numbers --hash of a pair of 32-bit numbers
return { return {

View File

@ -1,30 +1,23 @@
--CData structure that can hold the longest string representation of an IPv6 address
--we do this because LuaSocket expects a Lua string, and because we can't guarantee v4-only addresses
local ffi = assert( require 'ffi' ) local ffi = assert( require 'ffi' )
local ipString = {} local ipString = {}
local string = assert( string ) local string = assert( string )
ffi.cdef[[ ffi.cdef[[
typedef struct { typedef struct {
uint8_t a; char ip[40];
uint8_t b;
uint8_t c;
uint8_t d;
} ipAddress; } ipAddress;
]] ]]
local ipAddress = ffi.typeof( ffi.new( "ipAddress" ) ) local ipAddress = ffi.typeof( ffi.new( "ipAddress" ) )
ffi.metatype( ipAddress, { __tostring = function( ip ) return ip.a.."."..ip.b.."."..ip.c.."."..ip.d end } ) ffi.metatype( ipAddress, { __tostring = function( ip ) return ffi.string( ip.ip, ffi.sizeof( ip.ip ) ) end } )
function ipString.new( t ) function ipString.new( t )
local ip = ffi.new( ipAddress ) local ip = ffi.new( ipAddress )
ip.a, ip.b, ip.c, ip.d = t[1], t[2], t[3], t[4] ip.ip = t
return ip return ip
end end
function ipString.fromString( s ) ipString.fromString = ipString.new
local t = {}
for octet in s:gmatch( '%d+' ) do
table.insert( t, tonumber( octet ) )
end
return ipString.new( t )
end
return ipString return ipString

View File

@ -51,6 +51,7 @@ newStruct{
newStruct{ newStruct{
name = "connected", name = "connected",
"uint32_t token", "uint32_t token",
"uint32_t tick",
} }
newStruct{ newStruct{
@ -58,12 +59,6 @@ newStruct{
"char padding[300]" --Just a bunch of padding to mitigate amplification. "char padding[300]" --Just a bunch of padding to mitigate amplification.
} }
newStruct{
name = "heartbeat",
"uint32_t tick",
"uint32_t hash",
}
newStruct{ newStruct{
name = "insect", name = "insect",
"uint8_t id", "uint8_t id",
@ -93,6 +88,7 @@ newStruct{
newStruct{ newStruct{
name = "chatMessage", name = "chatMessage",
"uint8_t id",
"char cmsg[127]", "char cmsg[127]",
} }
@ -110,7 +106,7 @@ newStruct{
newStruct{ newStruct{
name = "disconnect", name = "disconnect",
"uint32_t reason", "char reason",
} }
local readBuffer = buffer.new( 1024 ) local readBuffer = buffer.new( 1024 )
@ -175,7 +171,7 @@ if testing then
svname = "😘😘😘😘😘😘😘kissyfaceserver", svname = "😘😘😘😘😘😘😘kissyfaceserver",
version = 25, version = 25,
port = 51312, port = 51312,
ip = ipString.new{ 132, 213, 45, 21 } ip = ipString.new '132.145.25.62'
} }
packet.heartbeat{ packet.heartbeat{