From 2b458b5df88b60f14daacd20152778de6679d9fd Mon Sep 17 00:00:00 2001 From: 20kdc Date: Sat, 18 Mar 2017 09:34:13 +0000 Subject: [PATCH] Add incomplete workspace, for now. --- .gitignore | 2 + LICENSE | 1 + culib.lua | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++++ gennet.lua | 81 +++++++++++++++++++++++++++++++++++ protocol.0 | 39 +++++++++++++++++ protocol.1 | 42 ++++++++++++++++++ protocol.2 | 93 ++++++++++++++++++++++++++++++++++++++++ runtest.lua | 13 ++++++ testnet.lua | 93 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 484 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 culib.lua create mode 100644 gennet.lua create mode 100644 protocol.0 create mode 100644 protocol.1 create mode 100644 protocol.2 create mode 100644 runtest.lua create mode 100644 testnet.lua diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3975246 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +netref.dot +netref.png diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..662df8c --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +I, 20kdc, release this into the public domain. diff --git a/culib.lua b/culib.lua new file mode 100644 index 0000000..eb3398f --- /dev/null +++ b/culib.lua @@ -0,0 +1,120 @@ +-- 'Copper' networking test implementation. +-- This is meant as a portable (even into OC) library for networking. +-- This 'outer function' is the instantiator. + +-- Note that it is probably possible to cause this code to run out of +-- memory in several hilarious ways. + +-- Interfaces have no meaning, since addresses are names. +-- Which "side" a system is on is irrelevant. +-- For sending, the following function is used: +-- transmit(nodeId, message) +-- The nodeId is a string or number that has been given via culib.input, +-- or nil for broadcast. +-- It's more of a suggestion than a requirement to check nodeId. + +-- The message is always a string. +-- This mirrors the message format usable by sendPacket and onReceive. + +-- "onReceive" is a function which is called when a packet is decoded. +-- onReceive(namefrom, nameto, data) + +-- "time" is a function which returns the real time, in seconds. +-- It need not be precise. +-- (This is used for caches.) +return function (hostname, transmit, onReceive, time) + + -- How many packets need to be stored in seenBefore's keyspace + -- before 'panic' is the best response? + local tuningMaxSeenBeforeCountBeforeEmergencyFlush = 0x100 + + -- Expect a response by this many seconds, + -- or else clear the known receivers cache and resend. + local tuningExpectResponse = 20 + + -- Flush the loop detector every so often. + -- This is not a complete clear. + local tuningFlushLoopDetector = 120 + + -- Do not change this value. I mean it. Don't. Just. Don't. + local tuningAutorejectLen = 4000 + + local loopDetectorNext = time() + tuningFlushLoopDetector + + -- Packets that have been seen before. + -- The values are the amount of times a packet has been seen. + -- This is flushed every tuningFlushLoopDetector seconds - + -- the flushing decrements the value until it reaches 0, + -- so packets which have looped before get a longer timeout. + local seenBefore = {} + local seenBeforeCount = 0 + + -- Unacknowledged packets. + -- Can cause an earlier cache flush. + -- [address] = giveupTime + -- (These are just forgotten after a while) + --local sentBNR = {}--NYI + + -- [address] = { + -- node, + -- expiry + -- } + --local lastKnownReceiver = {}--NYI + + local function refresh() + local t = time() + if t >= loopDetectorNext then + for k, v in pairs(seenBefore) do + local n = v - 1 + if n > 0 then + seenBefore[k] = n + else + seenBefore[k] = nil + seenBeforeCount = seenBeforeCount - 1 + end + end + loopDetectorNext = time() + tuningFlushLoopDetector + end + end + + local culib = {} + + -- Can be changed. + culib.hostname = hostname + culib.input = function (node, message) + if message:len() > tuningAutorejectLen then + return + end + if seenBefore[message] then + seenBefore[message] = seenBefore[message] + 1 + return + else + seenBefore[message] = 0 + seenBeforeCount = seenBeforeCount + 1 + if seenBeforeCount > tuningMaxSeenBeforeCountBeforeEmergencyFlush then + -- Panic + seenBeforeCount = 0 + seenBefore = {} + end + end + if message:len() < 2 then return end + local nlen = message:byte(1) + 1 + local fnam = message:sub(1, nlen) + message = message:sub(nlen + 1) + if message:len() < 2 then return end + local nlen = message:byte(1) + 1 + local tnam = message:sub(1, nlen) + message = message:sub(nlen + 1) + if message:len() < 1 then return end + onReceive(fnam, tnam, message) + if culib.hostname == tnam then return end + -- Redistribution of messages not aimed here + transmit(nil, message) + end + culib.output = function (fnam, tnam, message) + onReceive(fnam, tnam, message) + if tnam == culib.hostname then return end + transmit(nil, encodeName(fnam) .. encodeName(tnam) .. message) + end + return culib +end diff --git a/gennet.lua b/gennet.lua new file mode 100644 index 0000000..22c75c9 --- /dev/null +++ b/gennet.lua @@ -0,0 +1,81 @@ +-- Generate connected network where all nodes are connected. +-- Saves graph to "netref.dot", outputs lua tables to stdout. + +local nodes = {} +local wordsA = { + "changing", + "ponderous", + "intriguing", + "bright", + "solitudial", + "nuanced" +} +local wordsB = { + "fontaine", + "marple", + "poirot", + "pinkie", + "sparks", + "twi" +} +for i = 1, #wordsA do + for j = 1, #wordsB do + table.insert(nodes, wordsA[i] .. "_" .. wordsB[j]) + end +end + +local connections = {} +for i = 1, #nodes do + connections[i] = {} + connections[i][i] = true +end + +-- Recursive algorithm. +-- It will always come to the right answer, though. +-- (But definitely wouldn't ever return the fastest route.) +local function canRoute(i, j, avoid) + if i == j then return true end + if avoid[i] then return false end + avoid[i] = true + for p = 1, #nodes do + if connections[i][p] then + if canRoute(p, j, avoid) then + return true + end + end + end + return false +end + +local function ensureRoute(i, j) + while not canRoute(i, j, {}) do + local a, b = math.random(#nodes), math.random(#nodes) + connections[a][b] = true + connections[b][a] = true + end +end + +print("return function (declare, connect)") +for i = 1, #nodes do + -- Perform declaration here so next pass can do connections + print(" declare(\"" .. nodes[i] .. "\")") + for j = i + 1, #nodes do + ensureRoute(i, j) + end +end + +local dot = io.open("netref.dot", "w") +dot:write("graph \"Test Network Reference Graph\" {\n") +-- Notably this is a destructive process (to prevent backwards links) +for i = 1, #nodes do + for p = 1, #nodes do + if (p ~= i) and connections[i][p] then + print(" connect(" .. i .. ", " .. p .. ")") + dot:write(" " .. nodes[i] .. " -- " .. nodes[p] .. ";\n") + connections[p][i] = false + end + end +end +dot:write("}") +dot:close() +print("end") diff --git a/protocol.0 b/protocol.0 new file mode 100644 index 0000000..bf394e4 --- /dev/null +++ b/protocol.0 @@ -0,0 +1,39 @@ +Copper Protocol + 20kdc, 2017 + +Copper is a simple to implement networking protocol based on names. +This is it's sole purpose. +It can be used in various contexts, though it is not suitable as a + secure peer-to-peer networking protocol where all actors are untrusted. + +Rather, Copper is better for the situation of the current internet - + hierarchial structures (operated by semi-trusted parties, with + encryption used to hide information from them as appropriate), + with arbitary network structure at the fully-trusted-network level. + +Copper addresses are names. +In the context of a system not implementing a hierarchial gateway, + this is as much about Copper addressing as matters. + +Copper base packets contain 3 fields. +A name (as a length-minus-1-byte-prefixed-string), + another name (in the same format), + and the rest is data. + +Copper packets may be up to 4000 bytes long, including base. +(It's assumed the additional 96 bytes will be useful for any additional + framing, assuming a 4K packet limit.) + +Loop detection should performed by checking if a packet exactly the same has + been seen recently - other rejection, alteration and routing measures + are up to the implementer. + +Signalling is inadvisable - Copper is primarily meant to allow creating + internally "partyline" OpenComputers in-game networks with named nodes + and some semblance of routing or structure. + +Should a situation be dire enough, + hierarchial networks (described in file 2, 'protocol.1'), + and custom routing software in general, + can be used to split networks however the system requires. +Copper isn't very picky. diff --git a/protocol.1 b/protocol.1 new file mode 100644 index 0000000..b926b9b --- /dev/null +++ b/protocol.1 @@ -0,0 +1,42 @@ +Copper Protocol :: Hierarchial Gateways + 20kdc, 2017 + +"Hierarchial Gateways" are a system for ISP-like bodies to prevent their + users from causing havoc. +They are simply base low-level Copper nodes with two interfaces and the + following rules: + + For the FROM address: + +If it's on the parent side, reject if it's prefixed with hostname .. +"/", + otherwise prefix it with "<" and forward to child side. +If it's on the child side, reject if it's prefixed with "<", + otherwise prefix it with hostname .. "/" and forward to parent side. + + For the TO address: + +If it's on the parent side, reject unless prefixed with hostname .. "/", + otherwise remove that and forward to child side. +If it's on the child side, reject unless prefixed with "<", + otherwise remove that and forward to parent side. + +This introduces a simple hierarchial structure that does not require any + support code apart from in the nodes supporting the hierarchy. +There are only two downsides: + 1. Nodes connected to two gateways + could have multiple addresses but believe they only have one + 2. Nodes behind two nested gateways can address themselves in two ways + +However, this should all work out as long as people make a deliberate + effort not to notice the hierarchial structure system in their code. + +Specifically, problem 2 only causes an issue should some nodes not + directly involved in gateway activities be + attempting to parse hierarchial addresses. + +Possible uses of hierarchial gateways: +1. A safe (untrusting of servers in it) inter-server networking hub, + following the same protocol as everything else in Copper, + unlike certain competitors +2. ISPs within servers, perhaps those using the hubs diff --git a/protocol.2 b/protocol.2 new file mode 100644 index 0000000..12fb0df --- /dev/null +++ b/protocol.2 @@ -0,0 +1,93 @@ +Copper Protocol :: Reliability/Fragmentation Layer + 20kdc, 2017 + +The Copper Protocol as described in files 1 and 2 does not have any + semblance of application multiplexing or failure recovery. + +This is intentional. +Assuming that nobody is trying to make the fatal mistake of constructing + a NAT, files 1 and 2 are enough for all routing-related purposes. + +For applications, however, a protocol must be layered on top. + +This document on the Reliability Layer describes how that should work. + + +All implementations of Copper that synthesize their own packets SHOULD + follow this protocol when doing so, unless they are a custom system + that will not be connected to any global network. + + +Firstly, note that, to the application, a Reliability Layer packet can + be up to 59,895 bytes in size, though a fragment can only be up to 3993 bytes. + +Secondly, note that an application should be able to ask to be notified + when a packet is received successfully or when the implementation gives up, + with a flag indicating which is which. + +Reliability Layer packets have a simple format. +The first two bytes are the port number, in big-endian format. +The next three bytes are a number to this application-side packet. +They should be as random as possible. +The next byte is the 'attempt number' - the amount of attempts by this + side of the Reliability Layer "connection" to send a packet with this + meaning. + +This can be achieved serially or otherwise, but should have a random base. +Combined with correctly-forgetting packet caches, this should prevent + any packets lost by data collision. +The final header byte is the actual indicator of what is in the packet. + +The upper nibble indicates the amount of fragments in the packet - 0 + indicates an acknowledgement. +The lower nibble indicates which fragment this is, or if this is an + acknowledgement, which fragment was acknowledged. + +0x0F indicates that this is a *deliberately* unreliable packet. +(These packets cannot be fragmented or acknowledged, and thus have the + per-fragment limit of 3993 bytes. + The attempt number and primary packet number still have meaning.) + +Two example scenarioes will now be presented: + +1. + +ARCHWAYS sends a 0x10 'First fragment of a one fragment packet' to + IWAKURA on port 8080, twice (the first attempt being dropped). + 1F 90 | F4 21 B9 | 00/01 | 10 | (...) + port packetID Attempt CC Data + +IWAKURA receives it successfully on the second time, and sends back a + response, three times. + 1F 90 | F4 21 B9 | 00/01/02 | 00 + port packetID Attempt CC + +ARCHWAYS receives the response and does not send a third packet. + +2. + +IWAKURA, having parsed the packet, sends back a long response on the same port. +The response is two packets long. + 1F 90 | 91 19 28 | 00 | 20 | (...) + 1F 90 | 91 19 28 | 00 | 21 | (...) + +ARCHWAYS receives both packets, in the wrong order + (but it reassembles it anyway), and ACKs three times... + ...but the packets are dropped due to a crow getting in the way of the + satellite dish at the wrong point. Blasted crow. + + 1F 90 | 91 19 28 | 00/01/02 | 21 + 1F 90 | 91 19 28 | 00/01/02 | 20 + +IWAKURA, waiting, say, 6 seconds + (assuming ACKs are sent a second and a half apart) sends a retransmission. + + 1F 90 | 91 19 28 | 01 | 20 | (...) + 1F 90 | 91 19 28 | 01 | 21 | (...) + +ARCHWAYS ACKs the retransmission, just in case - this works. + + 1F 90 | 91 19 28 | 00/01/02 | 21 + 1F 90 | 91 19 28 | 00/01/02 | 20 + +IWAKURA's application knows the message got through. diff --git a/runtest.lua b/runtest.lua new file mode 100644 index 0000000..4e65480 --- /dev/null +++ b/runtest.lua @@ -0,0 +1,13 @@ +-- Load testnet +local nodes = {} +local nodenames = {} +loadfile("testnet.lua")(function (n) + table.insert(nodes, {}) + table.insert(nodenames, n) +end, function (a, b) + table.insert(nodes[a], b) + table.insert(nodes[b], a) +end) + +-- Start testing +require("culib") diff --git a/testnet.lua b/testnet.lua new file mode 100644 index 0000000..349d7cd --- /dev/null +++ b/testnet.lua @@ -0,0 +1,93 @@ +return function (declare, connect) + declare("changing_fontaine") + declare("changing_marple") + declare("changing_poirot") + declare("changing_pinkie") + declare("changing_sparks") + declare("changing_twi") + declare("ponderous_fontaine") + declare("ponderous_marple") + declare("ponderous_poirot") + declare("ponderous_pinkie") + declare("ponderous_sparks") + declare("ponderous_twi") + declare("intriguing_fontaine") + declare("intriguing_marple") + declare("intriguing_poirot") + declare("intriguing_pinkie") + declare("intriguing_sparks") + declare("intriguing_twi") + declare("bright_fontaine") + declare("bright_marple") + declare("bright_poirot") + declare("bright_pinkie") + declare("bright_sparks") + declare("bright_twi") + declare("solitudial_fontaine") + declare("solitudial_marple") + declare("solitudial_poirot") + declare("solitudial_pinkie") + declare("solitudial_sparks") + declare("solitudial_twi") + declare("nuanced_fontaine") + declare("nuanced_marple") + declare("nuanced_poirot") + declare("nuanced_pinkie") + declare("nuanced_sparks") + declare("nuanced_twi") + connect(1, 3) + connect(1, 9) + connect(2, 20) + connect(3, 17) + connect(3, 35) + connect(4, 5) + connect(4, 19) + connect(5, 29) + connect(6, 15) + connect(6, 22) + connect(6, 25) + connect(6, 32) + connect(7, 10) + connect(7, 24) + connect(8, 18) + connect(8, 33) + connect(8, 36) + connect(9, 17) + connect(9, 33) + connect(9, 35) + connect(10, 20) + connect(11, 13) + connect(11, 23) + connect(11, 27) + connect(11, 28) + connect(12, 29) + connect(12, 30) + connect(13, 24) + connect(13, 25) + connect(13, 28) + connect(13, 33) + connect(14, 19) + connect(14, 28) + connect(15, 21) + connect(15, 30) + connect(15, 31) + connect(15, 33) + connect(16, 24) + connect(16, 32) + connect(16, 34) + connect(18, 23) + connect(18, 36) + connect(19, 23) + connect(19, 25) + connect(19, 28) + connect(19, 31) + connect(22, 35) + connect(23, 26) + connect(24, 31) + connect(25, 33) + connect(26, 34) + connect(30, 34) + connect(31, 33) + connect(33, 35) + connect(34, 35) +end