diff --git a/nixos/modules/services/networking/cjdns-hosts.sh b/nixos/modules/services/networking/cjdns-hosts.sh
new file mode 100644
index 000000000000..8a2b47e52143
--- /dev/null
+++ b/nixos/modules/services/networking/cjdns-hosts.sh
@@ -0,0 +1,11 @@
+pubs=($pubs)
+hosts=($hosts)
+
+lines="''\n"
+for ((i = 0; i < ${#pubs[*]}; i++)); do
+ addr=$($cjdns/bin/publictoip6 ${pubs[i]})
+ lines="${lines}$addr ${hosts[i]}\n"
+done
+lines="${lines}''"
+
+echo -ne $lines > $out
diff --git a/nixos/modules/services/networking/cjdns.nix b/nixos/modules/services/networking/cjdns.nix
index 7192b8b7a0e0..9888419309c1 100644
--- a/nixos/modules/services/networking/cjdns.nix
+++ b/nixos/modules/services/networking/cjdns.nix
@@ -4,8 +4,46 @@ with lib;
let
+ pkg = pkgs.cjdns;
+
cfg = config.services.cjdns;
+ connectToSubmodule =
+ { options, ... }:
+ { options =
+ { password = mkOption {
+ type = types.str;
+ description = "Authorized password to the opposite end of the tunnel.";
+ };
+ publicKey = mkOption {
+ type = types.str;
+ description = "Public key at the opposite end of the tunnel.";
+ };
+ hostname = mkOption {
+ default = "";
+ example = "foobar.hype";
+ type = types.str;
+ description = "Optional hostname to add to /etc/hosts; prevents reverse lookup failures.";
+ };
+ };
+ };
+
+ peers = mapAttrsToList (n: v: v) (cfg.ETHInterface.connectTo // cfg.UDPInterface.connectTo);
+
+ pubs = toString (map (p: if p.hostname == "" then "" else p.publicKey) peers);
+ hosts = toString (map (p: if p.hostname == "" then "" else p.hostname) peers);
+
+ cjdnsHosts =
+ if hosts != "" then
+ import (pkgs.stdenv.mkDerivation {
+ name = "cjdns-hosts";
+ builder = ./cjdns-hosts.sh;
+
+ inherit (pkgs) cjdns;
+ inherit pubs hosts;
+ })
+ else "";
+
# would be nice to merge 'cfg' with a //,
# but the json nesting is wacky.
cjdrouteConf = builtins.toJSON ( {
@@ -44,7 +82,7 @@ in
enable = mkOption {
type = types.bool;
- default = false;
+ default = false;
description = ''
Whether to enable the cjdns network encryption
and routing engine. A file at /etc/cjdns.keys will
@@ -53,84 +91,80 @@ in
'';
};
+ confFile = mkOption {
+ type = types.str;
+ default = "";
+ example = "/etc/cjdroute.conf";
+ description = ''
+ Ignore all other cjdns options and load configuration from this file.
+ '';
+ };
+
authorizedPasswords = mkOption {
type = types.listOf types.str;
- default = [ ];
- example = [
+ default = [ ];
+ example = [
"snyrfgkqsc98qh1y4s5hbu0j57xw5s0"
- "z9md3t4p45mfrjzdjurxn4wuj0d8swv"
- "49275fut6tmzu354pq70sr5b95qq0vj"
+ "z9md3t4p45mfrjzdjurxn4wuj0d8swv"
+ "49275fut6tmzu354pq70sr5b95qq0vj"
];
- description = ''
- Any remote cjdns nodes that offer these passwords on
- connection will be allowed to route through this node.
+ description = ''
+ Any remote cjdns nodes that offer these passwords on
+ connection will be allowed to route through this node.
'';
};
admin = {
bind = mkOption {
type = types.string;
- default = "127.0.0.1:11234";
- description = ''
+ default = "127.0.0.1:11234";
+ description = ''
Bind the administration port to this address and port.
- '';
+ '';
};
};
UDPInterface = {
bind = mkOption {
type = types.string;
- default = "";
+ default = "";
example = "192.168.1.32:43211";
description = ''
- Address and port to bind UDP tunnels to.
- '';
- };
+ Address and port to bind UDP tunnels to.
+ '';
+ };
connectTo = mkOption {
- type = types.attrsOf ( types.submodule (
- { options, ... }:
- { options = {
- # TODO make host an option, and add it to networking.extraHosts
- password = mkOption {
- type = types.str;
- description = "Authorized password to the opposite end of the tunnel.";
- };
- publicKey = mkOption {
- type = types.str;
- description = "Public key at the opposite end of the tunnel.";
- };
- };
- }
- ));
- default = { };
+ type = types.attrsOf ( types.submodule ( connectToSubmodule ) );
+ default = { };
example = {
"192.168.1.1:27313" = {
- password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM";
+ hostname = "homer.hype";
+ password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM";
publicKey = "371zpkgs8ss387tmr81q04mp0hg1skb51hw34vk1cq644mjqhup0.k";
};
};
description = ''
- Credentials for making UDP tunnels.
- '';
- };
+ Credentials for making UDP tunnels.
+ '';
+ };
};
ETHInterface = {
bind = mkOption {
- default = "";
- example = "eth0";
- description = ''
- Bind to this device for native ethernet operation.
- '';
- };
+ default = "";
+ example = "eth0";
+ description = ''
+ Bind to this device for native ethernet operation.
+ '';
+ };
beacon = mkOption {
- type = types.int;
+ type = types.int;
default = 2;
description = ''
Auto-connect to other cjdns nodes on the same network.
Options:
- 0: Disabled.
+ 0: Disabled.
1: Accept beacons, this will cause cjdns to accept incoming
beacon messages and try connecting to the sender.
2: Accept and send beacons, this will cause cjdns to broadcast
@@ -142,32 +176,20 @@ in
};
connectTo = mkOption {
- type = types.attrsOf ( types.submodule (
- { options, ... }:
- { options = {
- password = mkOption {
- type = types.str;
- description = "Authorized password to the opposite end of the tunnel.";
- };
- publicKey = mkOption {
- type = types.str;
- description = "Public key at the opposite end of the tunnel.";
- };
- };
- }
- ));
- default = { };
+ type = types.attrsOf ( types.submodule ( connectToSubmodule ) );
+ default = { };
example = {
"01:02:03:04:05:06" = {
- password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM";
+ hostname = "homer.hype";
+ password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM";
publicKey = "371zpkgs8ss387tmr81q04mp0hg1skb51hw34vk1cq644mjqhup0.k";
};
};
- description = ''
- Credentials for connecting look similar to UDP credientials
+ description = ''
+ Credentials for connecting look similar to UDP credientials
except they begin with the mac address.
- '';
- };
+ '';
+ };
};
};
@@ -185,34 +207,48 @@ in
wantedBy = [ "multi-user.target" ];
after = [ "network-interfaces.target" ];
- script = ''
- source /etc/cjdns.keys
- echo '${cjdrouteConf}' | sed \
- -e "s/@CJDNS_ADMIN_PASSWORD@/$CJDNS_ADMIN_PASSWORD/g" \
- -e "s/@CJDNS_PRIVATE_KEY@/$CJDNS_PRIVATE_KEY/g" \
- | ${pkgs.cjdns}/bin/cjdroute
- '';
+ script = (
+ if cfg.confFile != "" then "${pkg}/bin/cjdroute < ${cfg.confFile}" else
+ ''
+ source /etc/cjdns.keys
+ echo '${cjdrouteConf}' | sed \
+ -e "s/@CJDNS_ADMIN_PASSWORD@/$CJDNS_ADMIN_PASSWORD/g" \
+ -e "s/@CJDNS_PRIVATE_KEY@/$CJDNS_PRIVATE_KEY/g" \
+ | ${pkg}/bin/cjdroute
+ ''
+ );
serviceConfig = {
Type = "forking";
- Restart = "on-failure";
+ Restart = "on-failure";
};
};
- system.activationScripts.cjdns = ''
+ system.activationScripts.cjdns = if (cfg.confFile == "") then "" else ''
+ cjdnsWriteKeys() {
+ private=$1
+ ipv6=$2
+ public=$3
+
+ echo "CJDNS_PRIVATE_KEY=$1" >> /etc/cjdns.keys
+ echo -e "CJDNS_IPV6=$2\nCJDNS_PUBLIC_KEY=$3" > /etc/cjdns.public
+
+ chmod 600 /etc/cjdns.keys
+ chmod 444 /etc/cjdns.public
+ }
+
grep -q "CJDNS_PRIVATE_KEY=" /etc/cjdns.keys || \
- echo "CJDNS_PRIVATE_KEY=$(${pkgs.cjdns}/bin/makekey)" \
- >> /etc/cjdns.keys
+ cjdnsWriteKeys $(${pkg}/bin/makekeys)
grep -q "CJDNS_ADMIN_PASSWORD=" /etc/cjdns.keys || \
- echo "CJDNS_ADMIN_PASSWORD=$(${pkgs.coreutils}/bin/head -c 96 /dev/urandom | ${pkgs.coreutils}/bin/tr -dc A-Za-z0-9)" \
- >> /etc/cjdns.keys
-
- chmod 600 /etc/cjdns.keys
+ echo "CJDNS_ADMIN_PASSWORD=$(${pkgs.coreutils}/bin/head -c 96 /dev/urandom | ${pkgs.coreutils}/bin/tr -dc A-Za-z0-9)" \
+ >> /etc/cjdns.keys
'';
+ networking.extraHosts = "${cjdnsHosts}";
+
assertions = [
- { assertion = ( cfg.ETHInterface.bind != "" || cfg.UDPInterface.bind != "" );
+ { assertion = ( cfg.ETHInterface.bind != "" || cfg.UDPInterface.bind != "" || cfg.confFile == "" );
message = "Neither cjdns.ETHInterface.bind nor cjdns.UDPInterface.bind defined.";
}
{ assertion = config.networking.enableIPv6;
diff --git a/pkgs/tools/networking/cjdns/default.nix b/pkgs/tools/networking/cjdns/default.nix
index c56faac690c3..9f734f2f6d0f 100644
--- a/pkgs/tools/networking/cjdns/default.nix
+++ b/pkgs/tools/networking/cjdns/default.nix
@@ -1,30 +1,31 @@
-{ stdenv, fetchgit, nodejs, which, python27 }:
+{ stdenv, fetchFromGitHub, nodejs, which, python27 }:
let
- date = "20140922";
- rev = "5ebca772b0582173127e8c1e61ee235c5ab3fb50";
+ date = "20140928";
+ rev = "e2b673698e471dbc82b4e9dbc04cb9e16f1f06a6";
in
stdenv.mkDerivation {
name = "cjdns-${date}-${stdenv.lib.strings.substring 0 7 rev}";
- src = fetchgit {
- url = "https://github.com/cjdelisle/cjdns.git";
+ src = fetchFromGitHub {
+ owner = "cjdelisle";
+ repo = "cjdns";
inherit rev;
- sha256 = "04abf73f4aede12c35b70ae09a367b3d6352a63f818185f788ed13356d06197a";
+ sha256 = "0ql51845rni6678dda03zr18ary7xlqcs3khva9x80x815h1sy8v";
};
+ patches = [ ./rfc5952.patch ];
+
buildInputs = [ which python27 nodejs];
- patches = [ ./makekey.patch ];
-
buildPhase = "bash do";
- installPhase = "installBin cjdroute makekey";
+ installPhase = "installBin cjdroute makekeys privatetopublic publictoip6";
- meta = {
+ meta = with stdenv.lib; {
homepage = https://github.com/cjdelisle/cjdns;
description = "Encrypted networking for regular people";
- license = stdenv.lib.licenses.gpl3;
- maintainers = with stdenv.lib.maintainers; [ viric emery ];
- platforms = stdenv.lib.platforms.linux;
+ license = licenses.gpl3;
+ maintainers = with maintainers; [ viric emery ];
+ platforms = platforms.unix;
};
}
diff --git a/pkgs/tools/networking/cjdns/makekey.patch b/pkgs/tools/networking/cjdns/makekey.patch
deleted file mode 100644
index fcce5e3e728e..000000000000
--- a/pkgs/tools/networking/cjdns/makekey.patch
+++ /dev/null
@@ -1,64 +0,0 @@
-diff --git a/contrib/c/makekey.c b/contrib/c/makekey.c
-new file mode 100644
-index 0000000..c7184e5
---- /dev/null
-+++ b/contrib/c/makekey.c
-@@ -0,0 +1,46 @@
-+/* vim: set expandtab ts=4 sw=4: */
-+/*
-+ * You may redistribute this program and/or modify it under the terms of
-+ * the GNU General Public License as published by the Free Software Foundation,
-+ * either version 3 of the License, or (at your option) any later version.
-+ *
-+ * This program is distributed in the hope that it will be useful,
-+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
-+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-+ * GNU General Public License for more details.
-+ *
-+ * You should have received a copy of the GNU General Public License
-+ * along with this program. If not, see .
-+ */
-+#include "crypto/random/Random.h"
-+#include "memory/MallocAllocator.h"
-+#include "crypto/AddressCalc.h"
-+#include "util/AddrTools.h"
-+#include "util/Hex.h"
-+
-+#include "crypto_scalarmult_curve25519.h"
-+
-+#include
-+
-+int main(int argc, char** argv)
-+{
-+ struct Allocator* alloc = MallocAllocator_new(1<<22);
-+ struct Random* rand = Random_new(alloc, NULL, NULL);
-+
-+ uint8_t privateKey[32];
-+ uint8_t publicKey[32];
-+ uint8_t ip[16];
-+ uint8_t hexPrivateKey[65];
-+
-+ for (;;) {
-+ Random_bytes(rand, privateKey, 32);
-+ crypto_scalarmult_curve25519_base(publicKey, privateKey);
-+ if (AddressCalc_addressForPublicKey(ip, publicKey)) {
-+ Hex_encode(hexPrivateKey, 65, privateKey, 32);
-+ printf(hexPrivateKey);
-+ return 0;
-+ }
-+ }
-+ return 0;
-+}
-+
-diff --git a/node_build/make.js b/node_build/make.js
-index 5e51645..11465e3 100644
---- a/node_build/make.js
-+++ b/node_build/make.js
-@@ -339,6 +339,7 @@ Builder.configure({
- builder.buildExecutable('contrib/c/privatetopublic.c');
- builder.buildExecutable('contrib/c/sybilsim.c');
- builder.buildExecutable('contrib/c/makekeys.c');
-+ builder.buildExecutable('contrib/c/makekey.c');
-
- builder.buildExecutable('crypto/random/randombytes.c');
-
diff --git a/pkgs/tools/networking/cjdns/rfc5952.patch b/pkgs/tools/networking/cjdns/rfc5952.patch
new file mode 100644
index 000000000000..2152d192b53a
--- /dev/null
+++ b/pkgs/tools/networking/cjdns/rfc5952.patch
@@ -0,0 +1,286 @@
+diff --git a/admin/angel/cjdroute2.c b/admin/angel/cjdroute2.c
+index dfce6c6..77954a7 100644
+--- a/admin/angel/cjdroute2.c
++++ b/admin/angel/cjdroute2.c
+@@ -80,7 +80,7 @@ static int genAddress(uint8_t addressOut[40],
+ if (AddressCalc_addressForPublicKey(address.ip6.bytes, address.key)) {
+ Hex_encode(privateKeyHexOut, 65, privateKey, 32);
+ Base32_encode(publicKeyBase32Out, 53, address.key, 32);
+- Address_printIp(addressOut, &address);
++ Address_printShortIp(addressOut, &address);
+ return 0;
+ }
+ }
+diff --git a/contrib/c/makekeys.c b/contrib/c/makekeys.c
+index 3727fff..29582f1 100644
+--- a/contrib/c/makekeys.c
++++ b/contrib/c/makekeys.c
+@@ -41,7 +41,7 @@ int main(int argc, char** argv)
+ if (AddressCalc_addressForPublicKey(ip, publicKey)) {
+ Hex_encode(hexPrivateKey, 65, privateKey, 32);
+ Base32_encode(publicKeyBase32, 53, publicKey, 32);
+- AddrTools_printIp(printedIp, ip);
++ AddrTools_printShortIp(printedIp, ip);
+ printf("%s %s %s.k\n", hexPrivateKey, printedIp, publicKeyBase32);
+ }
+ }
+diff --git a/contrib/c/privatetopublic.c b/contrib/c/privatetopublic.c
+index 7f5f967..dc98f1c 100644
+--- a/contrib/c/privatetopublic.c
++++ b/contrib/c/privatetopublic.c
+@@ -73,7 +73,7 @@ int main(int argc, char** argv)
+ AddressCalc_addressForPublicKey(address.ip6.bytes, address.key);
+ if (address.ip6.bytes[0] == 0xFC) {
+ Base32_encode(publicKeyBase32Out, 53, address.key, 32);
+- Address_printIp(addressOut, &address);
++ Address_printShortIp(addressOut, &address);
+ printf( "Input privkey: %s\n"
+ "Matching pubkey: %s.k\n"
+ "Resulting address: %s\n"
+diff --git a/contrib/c/publictoip6.c b/contrib/c/publictoip6.c
+index fc92f7e..99afc4c 100644
+--- a/contrib/c/publictoip6.c
++++ b/contrib/c/publictoip6.c
+@@ -48,7 +48,7 @@ int main(int argc, char** argv)
+ }
+
+ uint8_t output[40] = {0};
+- AddrTools_printIp(output, ip6Bytes);
++ AddrTools_printShortIp(output, ip6Bytes);
+ printf("%s\n", output);
+ return 0;
+ }
+diff --git a/dht/Address.c b/dht/Address.c
+index e4c2dba..ba77cad 100644
+--- a/dht/Address.c
++++ b/dht/Address.c
+@@ -102,6 +102,12 @@ void Address_printIp(uint8_t output[40], struct Address* addr)
+ AddrTools_printIp(output, addr->ip6.bytes);
+ }
+
++void Address_printShortIp(uint8_t output[40], struct Address* addr)
++{
++ Address_getPrefix(addr);
++ AddrTools_printShortIp(output, addr->ip6.bytes);
++}
++
+ void Address_print(uint8_t output[60], struct Address* addr)
+ {
+ Address_printIp(output, addr);
+diff --git a/dht/Address.h b/dht/Address.h
+index 43c6f05..f200b40 100644
+--- a/dht/Address.h
++++ b/dht/Address.h
+@@ -94,6 +94,8 @@ void Address_forKey(struct Address* out, const uint8_t key[Address_KEY_SIZE]);
+
+ void Address_printIp(uint8_t output[40], struct Address* addr);
+
++void Address_printShortIp(uint8_t output[40], struct Address* addr);
++
+ void Address_print(uint8_t output[60], struct Address* addr);
+
+ String* Address_toString(struct Address* addr, struct Allocator* alloc);
+diff --git a/net/Ducttape.c b/net/Ducttape.c
+index 84597d0..1813f3f 100644
+--- a/net/Ducttape.c
++++ b/net/Ducttape.c
+@@ -227,7 +227,7 @@ static inline bool isRouterTraffic(struct Message* message, struct Headers_IP6He
+ #define debugHandles(logger, session, message, ...) \
+ do { \
+ uint8_t ip[40]; \
+- AddrTools_printIp(ip, session->ip6); \
++ AddrTools_printIp(ip, session->ip6); \
+ Log_debug(logger, "ver[%u] send[%d] recv[%u] ip[%s] " message, \
+ session->version, \
+ Endian_hostToBigEndian32(session->sendHandle_be), \
+@@ -271,10 +271,10 @@ static inline uint8_t incomingForMe(struct Message* message,
+ if (Bits_memcmp(addr.ip6.bytes, dtHeader->ip6Header->sourceAddr, 16)) {
+ #ifdef Log_DEBUG
+ uint8_t keyAddr[40];
+- Address_printIp(keyAddr, &addr);
++ Address_printShortIp(keyAddr, &addr);
+ Bits_memcpyConst(addr.ip6.bytes, dtHeader->ip6Header->sourceAddr, 16);
+ uint8_t srcAddr[40];
+- Address_printIp(srcAddr, &addr);
++ Address_printShortIp(srcAddr, &addr);
+ Log_debug(context->logger,
+ "DROP packet because source address is not same as key.\n"
+ " %s source addr\n"
+@@ -292,7 +292,7 @@ static inline uint8_t incomingForMe(struct Message* message,
+ if (Checksum_udpIp6(dtHeader->ip6Header->sourceAddr, (uint8_t*)uh, message->length)) {
+ #ifdef Log_DEBUG
+ uint8_t keyAddr[40];
+- Address_printIp(keyAddr, &addr);
++ Address_printShortIp(keyAddr, &addr);
+ Log_debug(context->logger,
+ "DROP Router packet with incorrect checksum, from [%s]", keyAddr);
+ #endif
+@@ -708,7 +708,7 @@ static inline int core(struct Message* message,
+ struct Address destination;
+ Bits_memcpyConst(destination.ip6.bytes, ip6Header->destinationAddr, 16);
+ uint8_t ipAddr[40];
+- Address_printIp(ipAddr, &destination);
++ Address_printShortIp(ipAddr, &destination);
+ Log_debug(context->logger, "Forwarding data to %s via %s\n", ipAddr, nhAddr);
+ #endif */
+ } else {
+@@ -723,7 +723,7 @@ static inline int core(struct Message* message,
+ struct Address destination;
+ Bits_memcpyConst(destination.ip6.bytes, ip6Header->destinationAddr, 16);
+ uint8_t ipAddr[40];
+- Address_printIp(ipAddr, &destination);
++ Address_printShortIp(ipAddr, &destination);
+ Log_info(context->logger, "DROP message because this node is the closest known "
+ "node to the destination %s.", ipAddr);
+ #endif
+diff --git a/test/printIp_test.c b/test/printIp_test.c
+new file mode 100644
+index 0000000..75d7427
+--- /dev/null
++++ b/test/printIp_test.c
+@@ -0,0 +1,54 @@
++/* vim: set expandtab ts=4 sw=4: */
++/*
++ * You may redistribute this program and/or modify it under the terms of
++ * the GNU General Public License as published by the Free Software Foundation,
++ * either version 3 of the License, or (at your option) any later version.
++ *
++ * This program is distributed in the hope that it will be useful,
++ * but WITHOUT ANY WARRANTY; without even the implied warranty of
++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
++ * GNU General Public License for more details.
++ *
++ * You should have received a copy of the GNU General Public License
++ * along with this program. If not, see .
++ */
++
++#include "crypto/random/Random.h"
++#include "memory/MallocAllocator.h"
++#include "util/AddrTools.h"
++#include "util/Assert.h"
++
++#include
++
++int main()
++{
++ struct Allocator* alloc = MallocAllocator_new(1<<22);
++ struct Random* rand = Random_new(alloc, NULL, NULL);
++
++ uint8_t ip[16];
++ uint8_t printedIp[40];
++ uint8_t printedShortIp[40];
++ uint8_t ipFromFull[16];
++ uint8_t ipFromShort[16];
++
++ for (int i = 0; i < 1024; ++i) {
++ Random_bytes(rand, ip, 16);
++
++ for (int j = 0; j < 16; j++) {
++ // make the random result have lots of zeros since that's what we're looking for.
++ ip[j] = (ip[j] % 2) ? 0 : ip[j];
++ }
++
++ AddrTools_printIp(printedIp, ip);
++ AddrTools_printShortIp(printedShortIp, ip);
++ //printf("%s\n%s\n\n", printedIp, printedShortIp);
++
++ AddrTools_parseIp(ipFromFull, printedIp);
++ AddrTools_parseIp(ipFromShort, printedShortIp);
++
++ Assert_true(0 == Bits_memcmp(ip, ipFromFull, 16));
++ Assert_true(0 == Bits_memcmp(ipFromFull, ipFromShort, 16));
++ }
++
++ return 0;
++}
+diff --git a/util/AddrTools.h b/util/AddrTools.h
+index 858ced4..d59544d 100644
+--- a/util/AddrTools.h
++++ b/util/AddrTools.h
+@@ -143,6 +143,44 @@ static inline void AddrTools_printIp(uint8_t output[40], const uint8_t binIp[16]
+ output[39] = '\0';
+ }
+
++static inline void AddrTools_printShortIp(uint8_t output[40], const uint8_t binIp[16])
++{
++ /* The chances of hitting :0:0: and breaking
++ * RFC5952 are 1 in (1 / (2^16))^2 * 6.
++ * E. Siler
++ */
++
++ char *p = output;
++ int i = 0;
++ for (; i < 16;) {
++ if ((size_t)p != (size_t)output) {
++ *p++= ':';
++ }
++
++ if (binIp[i] > 0x0F) {
++ Hex_encode(p, 2, &binIp[i++], 1);
++ p += 2;
++ } else if (binIp[i] > 0x00) {
++ *p++ = Hex_encodeLowNibble(binIp[i++]);
++ } else {
++ ++i;
++ if (binIp[i] > 0x0F) {
++ Hex_encode(p, 2, &binIp[i++], 1);
++ p += 2;
++ } else {
++ *p++ = Hex_encodeLowNibble(binIp[i++]);
++ }
++ continue;
++ }
++ Hex_encode(p, 2, &binIp[i++], 1);
++ p += 2;
++ }
++ *p = '\0';
++
++ Assert_true((size_t)p <= ((size_t)output + 40));
++ Assert_true(i <= 16);
++}
++
+ /**
+ * Parse out an address.
+ *
+diff --git a/util/Hex.c b/util/Hex.c
+index e3e3c4d..b9bce57 100644
+--- a/util/Hex.c
++++ b/util/Hex.c
+@@ -29,6 +29,8 @@ static const uint8_t numForAscii[] =
+ 99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,
+ };
+
++static const char* hexEntities = "0123456789abcdef";
++
+ int Hex_encode(uint8_t* output,
+ const uint32_t outputLength,
+ const uint8_t* in,
+@@ -40,8 +42,6 @@ int Hex_encode(uint8_t* output,
+ output[inputLength * 2] = '\0';
+ }
+
+- static const char* hexEntities = "0123456789abcdef";
+-
+ for (uint32_t i = 0; i < inputLength; i++) {
+ output[i * 2] = hexEntities[in[i] >> 4];
+ output[i * 2 + 1] = hexEntities[in[i] & 15];
+@@ -88,3 +88,8 @@ int Hex_decode(uint8_t* output,
+
+ return length / 2;
+ }
++
++uint8_t Hex_encodeLowNibble(const uint8_t nibble)
++{
++ return hexEntities[nibble & 15];
++}
+diff --git a/util/Hex.h b/util/Hex.h
+index 4570c3e..a12e402 100644
+--- a/util/Hex.h
++++ b/util/Hex.h
+@@ -41,4 +41,6 @@ bool Hex_isHexEntity(const uint8_t character);
+
+ int Hex_decodeByte(const uint8_t highNibble, const uint8_t lowNibble);
+
++uint8_t Hex_encodeLowNibble(const uint8_t nibble);
++
+ #endif