Changeset

4428:0317b01b6bcd

Merge with Florob
author Matthew Wild <mwild1@gmail.com>
date Sat, 26 Nov 2011 03:50:51 +0000
parents 4423:ded726418b16 (diff) 4427:ae71ae5ddcfc (current diff)
children 4429:8e02ba75ef06
files
diffstat 7 files changed, 494 insertions(+), 38 deletions(-) [+]
line wrap: on
line diff
--- a/core/s2smanager.lua	Tue Nov 22 17:56:52 2011 +0000
+++ b/core/s2smanager.lua	Sat Nov 26 03:50:51 2011 +0000
@@ -16,8 +16,8 @@
 local format = string.format;
 local t_insert, t_sort = table.insert, table.sort;
 local get_traceback = debug.traceback;
-local tostring, pairs, ipairs, getmetatable, newproxy, error, tonumber, setmetatable
-    = tostring, pairs, ipairs, getmetatable, newproxy, error, tonumber, setmetatable;
+local tostring, pairs, ipairs, getmetatable, newproxy, next, error, tonumber, setmetatable
+    = tostring, pairs, ipairs, getmetatable, newproxy, next, error, tonumber, setmetatable;
 
 local idna_to_ascii = require "util.encodings".idna.to_ascii;
 local connlisteners_get = require "net.connlisteners".get;
@@ -28,6 +28,8 @@
 local stanza = st.stanza;
 local nameprep = require "util.encodings".stringprep.nameprep;
 local cert_verify_identity = require "util.x509".verify_identity;
+local new_ip = require "util.ip".new_ip;
+local rfc3484_dest = require "util.rfc3484".destination;
 
 local fire_event = prosody.events.fire_event;
 local uuid_gen = require "util.uuid".generate;
@@ -43,6 +45,7 @@
 local connect_timeout = config.get("*", "core", "s2s_timeout") or 60;
 local dns_timeout = config.get("*", "core", "dns_timeout") or 15;
 local max_dns_depth = config.get("*", "core", "dns_max_depth") or 3;
+local sources;
 
 dns.settimeout(dns_timeout);
 
@@ -243,6 +246,11 @@
 				for _, record in ipairs(answer) do
 					t_insert(srv_hosts, record.srv);
 				end
+				if #srv_hosts == 1 and srv_hosts[1].target == "." then
+					log("debug", to_host.." does not provide a XMPP service");
+					destroy_session(host_session, err); -- Nothing to see here
+					return;
+				end
 				t_sort(srv_hosts, compare_srv_priorities);
 				
 				local srv_choice = srv_hosts[1];
@@ -265,6 +273,8 @@
 		end, "_xmpp-server._tcp."..connect_host..".", "SRV");
 		
 		return true; -- Attempt in progress
+	elseif host_session.ip_hosts then
+		return try_connect(host_session, connect_host, connect_port, err);
 	elseif host_session.srv_hosts and #host_session.srv_hosts > host_session.srv_choice then -- Not our first attempt, and we also have SRV
 		host_session.srv_choice = host_session.srv_choice + 1;
 		local srv_choice = host_session.srv_hosts[host_session.srv_choice];
@@ -285,54 +295,138 @@
 	return try_connect(host_session, connect_host, connect_port);
 end
 
-function try_connect(host_session, connect_host, connect_port)
+function try_next_ip(host_session, connect_port)
+	host_session.connecting = nil;
+	host_session.ip_choice = host_session.ip_choice + 1;
+	local ip = host_session.ip_hosts[host_session.ip_choice];
+	local ok, err= make_connect(host_session, ip, connect_port);
+	if not ok then
+		if not attempt_connection(host_session, err or "closed") then
+			err = err and (": "..err) or "";
+			destroy_session(host_session, "Connection failed"..err);
+		end
+	end
+end
+
+function try_connect(host_session, connect_host, connect_port, err)
 	host_session.connecting = true;
-	local handle;
-	handle = adns.lookup(function (reply, err)
-		handle = nil;
-		host_session.connecting = nil;
-		
-		-- COMPAT: This is a compromise for all you CNAME-(ab)users :)
-		if not (reply and reply[#reply] and reply[#reply].a) then
-			local count = max_dns_depth;
-			reply = dns.peek(connect_host, "CNAME", "IN");
-			while count > 0 and reply and reply[#reply] and not reply[#reply].a and reply[#reply].cname do
-				log("debug", "Looking up %s (DNS depth is %d)", tostring(reply[#reply].cname), count);
-				reply = dns.peek(reply[#reply].cname, "A", "IN") or dns.peek(reply[#reply].cname, "CNAME", "IN");
-				count = count - 1;
+
+	if not err then
+		local IPs = {};
+		host_session.ip_hosts = IPs;
+		local handle4, handle6;
+		local has_other = false;
+
+		if not sources then
+			sources =  {};
+			local cfg_sources = connlisteners_get("xmppserver").default_interface or config.get("*", "core", "interface");
+			for i, source in ipairs(cfg_sources) do
+				if source == "*" then
+					sources[i] = new_ip("0.0.0.0", "IPv4");
+				else
+					sources[i] = new_ip(source, (source:find(":") and "IPv6") or "IPv4");
+				end
 			end
 		end
-		-- end of CNAME resolving
-		
-		if reply and reply[#reply] and reply[#reply].a then
-			log("debug", "DNS reply for %s gives us %s", connect_host, reply[#reply].a);
-			local ok, err = make_connect(host_session, reply[#reply].a, connect_port);
-			if not ok then
-				if not attempt_connection(host_session, err or "closed") then
-					err = err and (": "..err) or "";
-					destroy_session(host_session, "Connection failed"..err);
+
+		handle4 = adns.lookup(function (reply, err)
+			handle4 = nil;
+
+			-- COMPAT: This is a compromise for all you CNAME-(ab)users :)
+			if not (reply and reply[#reply] and reply[#reply].a) then
+				local count = max_dns_depth;
+				reply = dns.peek(connect_host, "CNAME", "IN");
+				while count > 0 and reply and reply[#reply] and not reply[#reply].a and reply[#reply].cname do
+					log("debug", "Looking up %s (DNS depth is %d)", tostring(reply[#reply].cname), count);
+					reply = dns.peek(reply[#reply].cname, "A", "IN") or dns.peek(reply[#reply].cname, "CNAME", "IN");
+					count = count - 1;
+				end
+			end
+			-- end of CNAME resolving
+
+			if reply and reply[#reply] and reply[#reply].a then
+				for _, ip in ipairs(reply) do
+					log("debug", "DNS reply for %s gives us %s", connect_host, ip.a);
+					IPs[#IPs+1] = new_ip(ip.a, "IPv4");
 				end
 			end
-		else
-			log("debug", "DNS lookup failed to get a response for %s", connect_host);
-			if not attempt_connection(host_session, "name resolution failed") then -- Retry if we can
-				log("debug", "No other records to try for %s - destroying", host_session.to_host);
-				err = err and (": "..err) or "";
-				destroy_session(host_session, "DNS resolution failed"..err); -- End of the line, we can't
+
+			if has_other then
+				if #IPs > 0 then
+					rfc3484_dest(host_session.ip_hosts, sources);
+					host_session.ip_choice = 0;
+					try_next_ip(host_session, connect_port);
+				else
+					log("debug", "DNS lookup failed to get a response for %s", connect_host);
+					host_session.ip_hosts = nil;
+					if not attempt_connection(host_session, "name resolution failed") then -- Retry if we can
+						log("debug", "No other records to try for %s - destroying", host_session.to_host);
+						err = err and (": "..err) or "";
+						destroy_session(host_session, "DNS resolution failed"..err); -- End of the line, we can't
+					end
+				end
+			else
+				has_other = true;
+			end
+		end, connect_host, "A", "IN");
+
+		handle6 = adns.lookup(function (reply, err)
+			handle6 = nil;
+
+			if reply and reply[#reply] and reply[#reply].aaaa then
+				for _, ip in ipairs(reply) do
+					log("debug", "DNS reply for %s gives us %s", connect_host, ip.aaaa);
+					IPs[#IPs+1] = new_ip(ip.aaaa, "IPv6");
+				end
 			end
+
+			if has_other then
+				if #IPs > 0 then
+					rfc3484_dest(host_session.ip_hosts, sources);
+					host_session.ip_choice = 0;
+					try_next_ip(host_session, connect_port);
+				else
+					log("debug", "DNS lookup failed to get a response for %s", connect_host);
+					host_session.ip_hosts = nil;
+					if not attempt_connection(host_session, "name resolution failed") then -- Retry if we can
+						log("debug", "No other records to try for %s - destroying", host_session.to_host);
+						err = err and (": "..err) or "";
+						destroy_session(host_session, "DNS resolution failed"..err); -- End of the line, we can't
+					end
+				end
+			else
+				has_other = true;
+			end
+		end, connect_host, "AAAA", "IN");
+
+		return true;
+	elseif host_session.ip_hosts and #host_session.ip_hosts > host_session.ip_choice then -- Not our first attempt, and we also have IPs left to try
+		try_next_ip(host_session, connect_port);
+	else
+		host_session.ip_hosts = nil;
+		if not attempt_connection(host_session, "out of IP addresses") then -- Retry if we can
+			log("debug", "No other records to try for %s - destroying", host_session.to_host);
+			err = err and (": "..err) or "";
+			destroy_session(host_session, "Connecting failed"..err); -- End of the line, we can't
+			return false;
 		end
-	end, connect_host, "A", "IN");
+	end
 
 	return true;
 end
 
 function make_connect(host_session, connect_host, connect_port)
-	(host_session.log or log)("info", "Beginning new connection attempt to %s (%s:%d)", host_session.to_host, connect_host, connect_port);
+	(host_session.log or log)("info", "Beginning new connection attempt to %s ([%s]:%d)", host_session.to_host, connect_host.addr, connect_port);
 	-- Ok, we're going to try to connect
 	
 	local from_host, to_host = host_session.from_host, host_session.to_host;
 	
-	local conn, handler = socket.tcp();
+	local conn, handler;
+	if connect_host.proto == "IPv4" then
+		conn, handler = socket.tcp();
+	else
+		conn, handler = socket.tcp6();
+	end
 	
 	if not conn then
 		log("warn", "Failed to create outgoing connection, system error: %s", handler);
@@ -340,14 +434,14 @@
 	end
 
 	conn:settimeout(0);
-	local success, err = conn:connect(connect_host, connect_port);
+	local success, err = conn:connect(connect_host.addr, connect_port);
 	if not success and err ~= "timeout" then
-		log("warn", "s2s connect() to %s (%s:%d) failed: %s", host_session.to_host, connect_host, connect_port, err);
+		log("warn", "s2s connect() to %s (%s:%d) failed: %s", host_session.to_host, connect_host.addr, connect_port, err);
 		return false, err;
 	end
 	
 	local cl = connlisteners_get("xmppserver");
-	conn = wrapclient(conn, connect_host, connect_port, cl, cl.default_mode or 1 );
+	conn = wrapclient(conn, connect_host.addr, connect_port, cl, cl.default_mode or 1 );
 	host_session.conn = conn;
 	
 	local filter = initialize_filters(host_session);
--- a/net/dns.lua	Tue Nov 22 17:56:52 2011 +0000
+++ b/net/dns.lua	Sat Nov 26 03:50:51 2011 +0000
@@ -358,6 +358,7 @@
 	local remember, pointers = nil, 0;
 	local len = self:byte();
 	local n = {};
+	if len == 0 then return "." end -- Root label
 	while len > 0 do
 		if len >= 0xc0 then    -- name is "compressed"
 			pointers = pointers + 1;
--- a/net/xmppserver_listener.lua	Tue Nov 22 17:56:52 2011 +0000
+++ b/net/xmppserver_listener.lua	Sat Nov 26 03:50:51 2011 +0000
@@ -178,7 +178,7 @@
 function xmppserver.ondisconnect(conn, err)
 	local session = sessions[conn];
 	if session then
-		if err and err ~= "closed" and session.srv_hosts then
+		if err and err ~= "closed" then
 			(session.log or log)("debug", "s2s connection attempt failed: %s", err);
 			if s2s_attempt_connect(session, err) then
 				(session.log or log)("debug", "...so we're going to try another target");
--- a/tests/test.lua	Tue Nov 22 17:56:52 2011 +0000
+++ b/tests/test.lua	Sat Nov 26 03:50:51 2011 +0000
@@ -12,6 +12,7 @@
 	package.loaded["net.connlisteners"] = { get = function () return {} end };
 	dotest "util.jid"
 	dotest "util.multitable"
+	dotest "util.rfc3484"
 	dotest "net.http"
 	dotest "core.modulemanager"
 	dotest "core.stanza_router"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_util_rfc3484.lua	Sat Nov 26 03:50:51 2011 +0000
@@ -0,0 +1,51 @@
+-- Prosody IM
+-- Copyright (C) 2011 Florian Zeitz
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+function source(source)
+	local new_ip = require"util.ip".new_ip;
+	assert_equal(source(new_ip("2001::1", "IPv6"), {new_ip("3ffe::1", "IPv6"), new_ip("fe80::1", "IPv6")}).addr, "3ffe::1", "prefer appropriate scope");
+	assert_equal(source(new_ip("2001::1", "IPv6"), {new_ip("fe80::1", "IPv6"), new_ip("fec0::1", "IPv6")}).addr, "fec0::1", "prefer appropriate scope");
+	assert_equal(source(new_ip("fec0::1", "IPv6"), {new_ip("fe80::1", "IPv6"), new_ip("2001::1", "IPv6")}).addr, "2001::1", "prefer appropriate scope");
+	assert_equal(source(new_ip("ff05::1", "IPv6"), {new_ip("fe80::1", "IPv6"), new_ip("fec0::1", "IPv6"), new_ip("2001::1", "IPv6")}).addr, "fec0::1", "prefer appropriate scope");
+	assert_equal(source(new_ip("2001::1", "IPv6"), {new_ip("2001::1", "IPv6"), new_ip("2002::1", "IPv6")}).addr, "2001::1", "prefer same address");
+	assert_equal(source(new_ip("fec0::1", "IPv6"), {new_ip("fec0::2", "IPv6"), new_ip("2001::1", "IPv6")}).addr, "fec0::2", "prefer appropriate scope");
+	assert_equal(source(new_ip("2001::1", "IPv6"), {new_ip("2001::2", "IPv6"), new_ip("3ffe::2", "IPv6")}).addr, "2001::2", "longest matching prefix");
+	assert_equal(source(new_ip("2002:836b:2179::1", "IPv6"), {new_ip("2002:836b:2179::d5e3:7953:13eb:22e8", "IPv6"), new_ip("2001::2", "IPv6")}).addr, "2002:836b:2179::d5e3:7953:13eb:22e8", "prefer matching label");
+end
+
+function destination(dest)
+	local order;
+	local new_ip = require"util.ip".new_ip;
+	order = dest({new_ip("2001::1", "IPv6"), new_ip("131.107.65.121", "IPv4")}, {new_ip("2001::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("169.254.13.78", "IPv4")})
+	assert_equal(order[1].addr, "2001::1", "prefer matching scope");
+	assert_equal(order[2].addr, "131.107.65.121", "prefer matching scope")
+
+	order = dest({new_ip("2001::1", "IPv6"), new_ip("131.107.65.121", "IPv4")}, {new_ip("fe80::1", "IPv6"), new_ip("131.107.65.117", "IPv4")})
+	assert_equal(order[1].addr, "131.107.65.121", "prefer matching scope")
+	assert_equal(order[2].addr, "2001::1", "prefer matching scope")
+
+	order = dest({new_ip("2001::1", "IPv6"), new_ip("10.1.2.3", "IPv4")}, {new_ip("2001::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("10.1.2.4", "IPv4")})
+	assert_equal(order[1].addr, "2001::1", "prefer higher precedence");
+	assert_equal(order[2].addr, "10.1.2.3", "prefer higher precedence");
+
+	order = dest({new_ip("2001::1", "IPv6"), new_ip("fec0::1", "IPv6"), new_ip("fe80::1", "IPv6")}, {new_ip("2001::2", "IPv6"), new_ip("fec0::1", "IPv6"), new_ip("fe80::2", "IPv6")})
+	assert_equal(order[1].addr, "fe80::1", "prefer smaller scope");
+	assert_equal(order[2].addr, "fec0::1", "prefer smaller scope");
+	assert_equal(order[3].addr, "2001::1", "prefer smaller scope");
+
+	order = dest({new_ip("2001::1", "IPv6"), new_ip("3ffe::1", "IPv6")}, {new_ip("2001::2", "IPv6"), new_ip("3f44::2", "IPv6"), new_ip("fe80::2", "IPv6")})
+	assert_equal(order[1].addr, "2001::1", "longest matching prefix");
+	assert_equal(order[2].addr, "3ffe::1", "longest matching prefix");
+
+	order = dest({new_ip("2002:836b:4179::1", "IPv6"), new_ip("2001::1", "IPv6")}, {new_ip("2002:836b:4179::2", "IPv6"), new_ip("fe80::2", "IPv6")})
+	assert_equal(order[1].addr, "2002:836b:4179::1", "prefer matching label");
+	assert_equal(order[2].addr, "2001::1", "prefer matching label");
+
+	order = dest({new_ip("2002:836b:4179::1", "IPv6"), new_ip("2001::1", "IPv6")}, {new_ip("2002:836b:4179::2", "IPv6"), new_ip("2001::2", "IPv6"), new_ip("fe80::2", "IPv6")})
+	assert_equal(order[1].addr, "2001::1", "prefer higher precedence");
+	assert_equal(order[2].addr, "2002:836b:4179::1", "prefer higher precedence");
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/ip.lua	Sat Nov 26 03:50:51 2011 +0000
@@ -0,0 +1,176 @@
+-- Prosody IM
+-- Copyright (C) 2008-2011 Florian Zeitz
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local ip_methods = {};
+local ip_mt = { __index = function (ip, key) return (ip_methods[key])(ip); end,
+		__tostring = function (ip) return ip.addr; end,
+		__eq = function (ipA, ipB) return ipA.addr == ipB.addr; end};
+local hex2bits = { ["0"] = "0000", ["1"] = "0001", ["2"] = "0010", ["3"] = "0011", ["4"] = "0100", ["5"] = "0101", ["6"] = "0110", ["7"] = "0111", ["8"] = "1000", ["9"] = "1001", ["A"] = "1010", ["B"] = "1011", ["C"] = "1100", ["D"] = "1101", ["E"] = "1110", ["F"] = "1111" };
+
+local function new_ip(ipStr, proto)
+	if proto ~= "IPv4" and proto ~= "IPv6" then
+		return nil, "invalid protocol";
+	end
+
+	return setmetatable({ addr = ipStr, proto = proto }, ip_mt);
+end
+
+local function toBits(ip)
+	local result = "";
+	local fields = {};
+	if ip.proto == "IPv4" then
+		ip = ip.toV4mapped;
+	end
+	ip = (ip.addr):upper();
+	ip:gsub("([^:]*):?", function (c) fields[#fields + 1] = c end);
+	if not ip:match(":$") then fields[#fields] = nil; end
+	for i, field in ipairs(fields) do
+		if field:len() == 0 and i ~= 1 and i ~= #fields then
+			for i = 1, 16 * (9 - #fields) do
+				result = result .. "0";
+			end
+		else
+			for i = 1, 4 - field:len() do
+				result = result .. "0000";
+			end
+			for i = 1, field:len() do
+				result = result .. hex2bits[field:sub(i,i)];
+			end
+		end
+	end
+	return result;
+end
+
+local function commonPrefixLength(ipA, ipB)
+	ipA, ipB = toBits(ipA), toBits(ipB);
+	for i = 1, 128 do
+		if ipA:sub(i,i) ~= ipB:sub(i,i) then
+			return i-1;
+		end
+	end
+	return 128;
+end
+
+local function v4scope(ip)
+	local fields = {};
+	ip:gsub("([^.]*).?", function (c) fields[#fields + 1] = tonumber(c) end);
+	-- Loopback:
+	if fields[1] == 127 then
+		return 0x2;
+	-- Link-local unicast:
+	elseif fields[1] == 169 and fields[2] == 254 then
+		return 0x2;
+	-- Site-local unicast:
+	elseif (fields[1] == 10) or (fields[1] == 192 and fields[2] == 168) or (fields[1] == 172 and fields[2] > 16) then
+		return 0x5;
+	-- Global unicast:
+	else
+		return 0xE;
+	end
+end
+
+local function v6scope(ip)
+	-- Loopback:
+	if ip:match("^[0:]*1$") then
+		return 0x2;
+	-- Link-local unicast:
+	elseif ip:match("^[Ff][Ee][89ABab]") then 
+		return 0x2;
+	-- Site-local unicast:
+	elseif ip:match("^[Ff][Ee][CcDdEeFf]") then
+		return 0x5;
+	-- Multicast:
+	elseif ip:match("^[Ff][Ff]") then
+		return tonumber("0x"..ip:sub(4,4));
+	-- Global unicast:
+	else
+		return 0xE;
+	end
+end
+
+local function label(ip)
+	if commonPrefixLength(ip, new_ip("::1", "IPv6")) == 128 then
+		return 0;
+	elseif commonPrefixLength(ip, new_ip("2002::", "IPv6")) >= 16 then
+		return 2;
+	elseif commonPrefixLength(ip, new_ip("::", "IPv6")) >= 96 then
+		return 3;
+	elseif commonPrefixLength(ip, new_ip("::ffff:0:0", "IPv6")) >= 96 then
+		return 4;
+	else
+		return 1;
+	end
+end
+
+local function precedence(ip)
+	if commonPrefixLength(ip, new_ip("::1", "IPv6")) == 128 then
+		return 50;
+	elseif commonPrefixLength(ip, new_ip("2002::", "IPv6")) >= 16 then
+		return 30;
+	elseif commonPrefixLength(ip, new_ip("::", "IPv6")) >= 96 then
+		return 20;
+	elseif commonPrefixLength(ip, new_ip("::ffff:0:0", "IPv6")) >= 96 then
+		return 10;
+	else
+		return 40;
+	end
+end
+
+local function toV4mapped(ip)
+	local fields = {};
+	local ret = "::ffff:";
+	ip:gsub("([^.]*).?", function (c) fields[#fields + 1] = tonumber(c) end);
+	ret = ret .. ("%02x"):format(fields[1]);
+	ret = ret .. ("%02x"):format(fields[2]);
+	ret = ret .. ":"
+	ret = ret .. ("%02x"):format(fields[3]);
+	ret = ret .. ("%02x"):format(fields[4]);
+	return new_ip(ret, "IPv6");
+end
+
+function ip_methods:toV4mapped()
+	if self.proto ~= "IPv4" then return nil, "No IPv4 address" end
+	local value = toV4mapped(self.addr);
+	self.toV4mapped = value;
+	return value;
+end
+
+function ip_methods:label()
+	local value;
+	if self.proto == "IPv4" then
+		value = label(self.toV4mapped);
+	else
+		value = label(self);
+	end
+	self.label = value;
+	return value;
+end
+
+function ip_methods:precedence()
+	local value;
+	if self.proto == "IPv4" then
+		value = precedence(self.toV4mapped);
+	else
+		value = precedence(self);
+	end
+	self.precedence = value;
+	return value;
+end
+
+function ip_methods:scope()
+	local value;
+	if self.proto == "IPv4" then
+		value = v4scope(self.addr);
+	else
+		value = v6scope(self.addr);
+	end
+	self.scope = value;
+	return value;
+end
+
+return {new_ip = new_ip,
+	commonPrefixLength = commonPrefixLength};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/rfc3484.lua	Sat Nov 26 03:50:51 2011 +0000
@@ -0,0 +1,133 @@
+-- Prosody IM
+-- Copyright (C) 2008-2011 Florian Zeitz
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local commonPrefixLength = require"util.ip".commonPrefixLength
+local new_ip = require"util.ip".new_ip;
+
+local function t_sort(t, comp)
+	for i = 1, (#t - 1) do
+		for j = (i + 1), #t do
+			local a, b = t[i], t[j];
+			if not comp(a,b) then
+				t[i], t[j] = b, a;
+			end
+		end
+	end
+end
+
+function source(dest, candidates)
+	local function comp(ipA, ipB)
+		-- Rule 1: Prefer same address
+		if dest == ipA then
+			return true;
+		elseif dest == ipB then
+			return false;
+		end
+
+		-- Rule 2: Prefer appropriate scope
+		if ipA.scope < ipB.scope then
+			if ipA.scope < dest.scope then
+				return false;
+			else
+				return true;
+			end
+		elseif ipA.scope > ipB.scope then
+			if ipB.scope < dest.scope then
+				return true;
+			else
+				return false;
+			end
+		end
+
+		-- Rule 3: Avoid deprecated addresses
+		-- XXX: No way to determine this
+		-- Rule 4: Prefer home addresses
+		-- XXX: Mobility Address related, no way to determine this
+		-- Rule 5: Prefer outgoing interface
+		-- XXX: Interface to address relation. No way to determine this
+		-- Rule 6: Prefer matching label
+		if ipA.label == dest.label and ipB.label ~= dest.label then
+			return true;
+		elseif ipB.label == dest.label and ipA.label ~= dest.label then
+			return false;
+		end
+
+		-- Rule 7: Prefer public addresses (over temporary ones)
+		-- XXX: No way to determine this
+		-- Rule 8: Use longest matching prefix
+		if commonPrefixLength(ipA, dest) > commonPrefixLength(ipB, dest) then
+			return true;
+		else
+			return false;
+		end
+	end
+
+	t_sort(candidates, comp);
+	return candidates[1];
+end
+
+function destination(candidates, sources)
+	local sourceAddrs = {};
+	local function comp(ipA, ipB)
+		local ipAsource = sourceAddrs[ipA];
+		local ipBsource = sourceAddrs[ipB];
+		-- Rule 1: Avoid unusable destinations
+		-- XXX: No such information
+		-- Rule 2: Prefer matching scope
+		if ipA.scope == ipAsource.scope and ipB.scope ~= ipBsource.scope then
+			return true;
+		elseif ipA.scope ~= ipAsource.scope and ipB.scope == ipBsource.scope then
+			return false;
+		end
+
+		-- Rule 3: Avoid deprecated addresses
+		-- XXX: No way to determine this
+		-- Rule 4: Prefer home addresses
+		-- XXX: Mobility Address related, no way to determine this
+		-- Rule 5: Prefer matching label
+		if ipAsource.label == ipA.label and ipBsource.label ~= ipB.label then
+			return true;
+		elseif ipBsource.label == ipB.label and ipAsource.label ~= ipA.label then
+			return false;
+		end
+
+		-- Rule 6: Prefer higher precedence
+		if ipA.precedence > ipB.precedence then
+			return true;
+		elseif ipA.precedence < ipB.precedence then
+			return false;
+		end
+
+		-- Rule 7: Prefer native transport
+		-- XXX: No way to determine this
+		-- Rule 8: Prefer smaller scope
+		if ipA.scope < ipB.scope then
+			return true;
+		elseif ipA.scope > ipB.scope then
+			return false;
+		end
+
+		-- Rule 9: Use longest matching prefix
+		if commonPrefixLength(ipA, ipAsource) > commonPrefixLength(ipB, ipBsource) then
+			return true;
+		elseif commonPrefixLength(ipA, ipAsource) < commonPrefixLength(ipB, ipBsource) then
+			return false;
+		end
+
+		-- Rule 10: Otherwise, leave order unchanged
+		return true;
+	end
+	for _, ip in ipairs(candidates) do
+		sourceAddrs[ip] = source(ip, sources);
+	end
+
+	t_sort(candidates, comp);
+	return candidates;
+end
+
+return {source = source,
+	destination = destination};