Skip to content

Commit

Permalink
Merge pull request #242 from alexandergall/datagram-new
Browse files Browse the repository at this point in the history
Partial IPv6 neighbor discovery and various performance tweaks
  • Loading branch information
lukego committed Aug 26, 2014
2 parents 8d9aa93 + 611017a commit 3c1a6de
Show file tree
Hide file tree
Showing 8 changed files with 396 additions and 90 deletions.
245 changes: 245 additions & 0 deletions src/apps/ipv6/nd_light.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
-- This app implements a small subset of IPv6 neighbor discovery
-- (RFC4861). It has two ports, north and south. The south port
-- attaches to a port on which ND must be performed. The north port
-- attaches to any app that transmits IPv6 packets and receives
-- Ethernet packets (in a future version, the app should strip the
-- Ethernet header to behave more like a true Ethernet layer).
--
-- The app replies to neighbor solicitations for which it is
-- configured as target and performs rudimentary address resolution
-- for its configured "next-hop" address. This is done by
-- transmitting a neighbor solicitation for the hext-hop with a
-- configurable number of retransmits (default 10) with a configurable
-- interval (default 1000ms) and processing the (solicited) neighbor
-- advertisements.
--
-- If address resolution succeeds, the app constructs an Ethernet
-- header and attaches it to all frames received from the north port.
-- The resulting packets are transmitted to the south port. All
-- packets from the north port are discarded as long as ND has not yet
-- succeeded.
--
-- Address resolution is not repeated for the lifetime of the app.
-- The app terminates if address resolution has not succeeded after
-- all retransmits have been performed.

module(..., package.seeall)
local ffi = require("ffi")
local C = ffi.C
local app = require("core.app")
local link = require("core.link")
local packet = require("core.packet")
local datagram = require("lib.protocol.datagram")
local ethernet = require("lib.protocol.ethernet")
local ipv6 = require("lib.protocol.ipv6")
local icmp = require("lib.protocol.icmp.header")
local ns = require("lib.protocol.icmp.nd.ns")
local na = require("lib.protocol.icmp.nd.na")
local filter = require("lib.pcap.filter")
local timer = require("core.timer")

local nd_light = subClass(nil)
nd_light._name = "Partial IPv6 neighbor discovery"

-- config:
-- local_mac MAC address of the interface attached to "south"
-- local_ip IPv6 address of the interface
-- next_hop IPv6 address of next-hop for all packets to south
-- delay NS retransmit delay in ms (default 1000ms)
-- retrans Number of NS retransmits (default 10)
function nd_light:new (config)
local o = nd_light:superClass().new(self)
config.delay = config.delay or 1000
config.retrans = config.retrans or 10
o._config = config
o._match_ns = function(ns)
return(ns:target_eq(config.local_ip))
end
o._match_na = function(na)
return(na:target_eq(config.next_hop) and na:solicited())
end
local errmsg
o._filter, errmsg = filter:new("icmp6 and ( ip6[40] = 135 or ip6[40] = 136 )")
assert(o._filter, errmsg and ffi.string(errmsg))

-- Prepare packet for solicitation of next hop
local nh = { nsent = 0 }
local dgram = datagram:new()
nh.packet = dgram:packet()
packet.tenure(nh.packet)
local sol_node_mcast = ipv6:solicited_node_mcast(config.next_hop)
local ipv6 = ipv6:new({ next_header = 58, -- ICMP6
hop_limit = 255,
src = config.local_ip,
dst = sol_node_mcast })
local icmp = icmp:new(135, 0)
local ns = ns:new(config.next_hop)
-- Note: we should include a source link-layer option here,
-- but the current ND stuff from lib/protocol doesn't provide a
-- simple enough mechanism to do this yet.
dgram:push(ns)
icmp:checksum(ns:header(), ns:sizeof(), ipv6)
dgram:push(icmp)
ipv6:payload_length(icmp:sizeof() + ns:sizeof())
dgram:push(ipv6)
dgram:push(ethernet:new({ src = config.local_mac,
dst = ethernet:ipv6_mcast(sol_node_mcast),
type = 0x86dd }))
dgram:free()

-- Timer for retransmits of neighbor solicitations
nh.timer_cb = function (t)
local nh = o._next_hop
print(string.format("Sending neighbor solicitation for next-hop %s",
ipv6:ntop(config.next_hop)))
link.transmit(o.output.south, nh.packet)
nh.nsent = nh.nsent + 1
if nh.nsent <= o._config.retrans and not o._eth_header then
timer.activate(nh.timer)
end
if nh.nsent > o._config.retrans then
error(string.format("ND for next hop %s has failed",
ipv6:ntop(config.next_hop)))
end
end
nh.timer = timer.new("ns retransmit", nh.timer_cb, 1e6 * config.delay)
self._next_hop = nh
timer.init()
self._dgram = datagram:new()
packet.deref(self._dgram:packet())
return o
end

-- Process neighbor solicitation
local function ns (self, dgram, eth, ipv6, icmp)
local payload, length = dgram:payload()
if not icmp:checksum_check(payload, length, ipv6) then
print(self:name()..": bad icmp checksum")
return nil
end
-- Parse the neighbor solicitation and check if it contains our own
-- address as target
local ns = dgram:parse(nil, self._match_ns)
if not ns then
return nil
end
local option = ns:options(dgram:payload())
if not (#option == 1 and option[1]:type() == 1) then
-- Invalid NS, ignore
return nil
end
-- Turn this message into a solicited neighbor
-- advertisement with target ll addr option

-- Ethernet
eth:swap()
eth:src(self._config.local_mac)

-- IPv6
ipv6:dst(ipv6:src())
ipv6:src(self._config.local_ip)

-- ICMP
option[1]:type(2)
option[1]:option():addr(self._config.local_mac)
icmp:type(136)
-- Undo/redo icmp and ns headers to get
-- payload and set solicited flag
dgram:unparse(2)
dgram:parse() -- icmp
local payload, length = dgram:payload()
dgram:parse():solicited(1)
icmp:checksum(payload, length, ipv6)
return true
end

-- Process neighbor advertisement
local function na (self, dgram, eth, ipv6, icmp)
if self._eth_header then
return nil
end
local na = dgram:parse(nil, self._match_na)
if not na then
return nil
end
local option = na:options(dgram:payload())
if not (#option == 1 and option[1]:type() == 2) then
-- Invalid NS, ignore
return nil
end
self._eth_header = ethernet:new({ src = self._config.local_mac,
dst = option[1]:option():addr(),
type = 0x86dd })
print(string.format("Resolved next-hop %s to %s", ipv6:ntop(self._config.next_hop),
ethernet:ntop(option[1]:option():addr())))
return nil
end

local function from_south (self, p)
local iov = p.iovecs[0]
if not self._filter:match(iov.buffer.pointer + iov.offset,
iov.length) then
return false
end
local dgram = datagram:new(p, ethernet)
-- Parse the ethernet, ipv6 amd icmp headers
dgram:parse_n(3)
local eth, ipv6, icmp = unpack(dgram:stack())
if ipv6:hop_limit() ~= 255 then
-- Avoid off-link spoofing as per RFC
return nil
end
local result
if icmp:type() == 135 then
result = ns(self, dgram, eth, ipv6, icmp)
else
result = na(self, dgram, eth, ipv6, icmp)
end
dgram:free()
return result
end

function nd_light:push ()
if self._next_hop.nsent == 0 then
-- Kick off address resolution
self._next_hop.timer_cb()
end

local l_in = self.input.south
local l_out = self.output.north
local l_reply = self.output.south
while not link.empty(l_in) and not link.full(l_out) do
local p = link.receive(l_in)
local status = from_south(self, p)
if status == nil then
-- Discard
packet.deref(p)
elseif status == true then
-- Send NA back south
link.transmit(l_reply, p)
else
-- Send transit traffic up north
-- XXX We should remove the Ethernet header here
-- to make this app behave more like a true
-- Ethernet layer.
link.transmit(l_out, p)
end
end

l_in = self.input.north
l_out = self.output.south
while not link.empty(l_in) and not link.full(l_out) do
if not self._eth_header then
-- Drop packets until ND for the next-hop
-- has completed.
packet.deref(link.receive(l_in))
else
local p = link.receive(l_in)
self._dgram:reuse(p)
self._dgram:push(self._eth_header)
link.transmit(l_out, p)
end
end
end

return nd_light
12 changes: 7 additions & 5 deletions src/apps/ipv6/ns_responder.lua
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,17 @@ function ns_responder:new(config)
local filter, errmsg = filter:new("icmp6 and ip6[40] = 135")
assert(filter, errmsg and ffi.string(errmsg))
o._filter = filter
o._dgram = datagram:new()
packet.deref(o._dgram:packet())
return o
end

local function process(self, dgram)
if not self._filter:match(dgram:payload()) then
local function process (self, p)
local iov = p.iovecs[0]
if not self._filter:match(iov.buffer.pointer + iov.offset, iov.length) then
return false
end
local dgram = self._dgram:reuse(p, ethernet)
-- Parse the ethernet, ipv6 amd icmp headers
dgram:parse_n(3)
local eth, ipv6, icmp = unpack(dgram:stack())
Expand Down Expand Up @@ -93,8 +97,7 @@ function ns_responder:push()
local l_reply = self.output.south
while not link.empty(l_in) and not link.full(l_out) do
local p = packet.want_modify(link.receive(l_in))
local datagram = datagram:new(p, ethernet)
local status = process(self, datagram)
local status = process(self, p)
if status == nil then
-- Discard
packet.deref(p)
Expand All @@ -105,7 +108,6 @@ function ns_responder:push()
-- Send transit traffic up north
link.transmit(l_out, p)
end
datagram:free()
end
end

Expand Down
2 changes: 1 addition & 1 deletion src/apps/socket/raw.lua
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ function RawSocket.selftest ()
local src = ethernet:pton("00:00:00:00:00:01")
local dst = ethernet:pton("00:00:00:00:00:02")
local localhost = ipv6:pton("0:0:0:0:0:0:0:1")
dg_tx:push(ethernet:new({src = src, dst = dst, type = 0x86dd}))
dg_tx:push(ipv6:new({src = localhost,
dst = localhost,
next_header = 59, -- no next header
hop_limit = 1}))
dg_tx:push(ethernet:new({src = src, dst = dst, type = 0x86dd}))

local link = require("core.link")
local lo = RawSocket:new("lo")
Expand Down
20 changes: 15 additions & 5 deletions src/apps/vpn/vpws.lua
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,26 @@ function vpws:new(config)
o._config = config
o._name = config.name
o._encap = {
ether = ethernet:new({ src = config.local_mac, dst = config.remote_mac, type = 0x86dd }),
ipv6 = ipv6:new({ next_header = 47, hop_limit = 64, src = config.local_vpn_ip,
dst = config.remote_vpn_ip}),
gre = gre:new({ protocol = 0x6558, checksum = config.checksum, key = config.label })
}
if config.remote_mac then
-- If the MAC address of the peer is not set, we assume that
-- some form of dynamic neighbor discovery is in effect
-- (e.g. through the nd_light app), which adds the ethernet header
-- separately
o._encap.ether = ethernet:new({ src = config.local_mac, dst = config.remote_mac,
type = 0x86dd })
end
-- Pre-computed size of combined Ethernet and IPv6 header
o._eth_ipv6_size = ethernet:sizeof() + ipv6:sizeof()
local program = "ip6 and dst host "..ipv6:ntop(config.local_vpn_ip) .." and ip6 proto 47"
local filter, errmsg = filter:new(program)
assert(filter, errmsg and ffi.string(errmsg))
o._filter = filter
o._dgram = datagram:new()
packet.deref(o._dgram:packet())
return o
end

Expand All @@ -49,7 +58,7 @@ function vpws:push()
assert(l_out)
while not link.full(l_out) and not link.empty(l_in) do
local p = link.receive(l_in)
local datagram = datagram:new(p, ethernet)
local datagram = self._dgram:reuse(p, ethernet)
if port_in == 'customer' then
local encap = self._encap
-- Encapsulate Ethernet frame coming in on customer port
Expand All @@ -60,9 +69,11 @@ function vpws:push()
encap.gre:checksum(datagram:payload())
end
-- Copy the finished headers into the packet
datagram:push(encap.ether)
datagram:push(encap.ipv6)
datagram:push(encap.gre)
datagram:push(encap.ipv6)
if encap.ether then
datagram:push(encap.ether)
end
else
-- Check for encapsulated frame coming in on uplink
if self._filter:match(datagram:payload()) then
Expand Down Expand Up @@ -101,7 +112,6 @@ function vpws:push()
end
end
if p then link.transmit(l_out, p) end
datagram:free()
end
end
end
Expand Down
Loading

0 comments on commit 3c1a6de

Please sign in to comment.