commit 2b458b5df88b60f14daacd20152778de6679d9fd Author: 20kdc Date: Sat Mar 18 09:34:13 2017 +0000 Add incomplete workspace, for now. 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