diff --git a/.travis.yml b/.travis.yml index 4d6da8357e..ad0efe0604 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,4 +9,6 @@ before_script: script: - make -j - - (cd src && sudo ./snabb snsh program/vita/test.snabb IMIX 1e6 3) + - (cd src && sudo program/vita/selftest.snabb) + - (cd src && sudo program/vita/conftest.snabb) + - (cd src && sudo program/vita/test.snabb IMIX 1e6 3) diff --git a/src/apps/test/match.lua b/src/apps/test/match.lua index 57d48f7453..fcb09591e1 100644 --- a/src/apps/test/match.lua +++ b/src/apps/test/match.lua @@ -30,7 +30,10 @@ function Match:push () elseif cmp.length ~= p.length or C.memcmp(cmp.data, p.data, cmp.length) ~= 0 then if not self.fuzzy then - table.insert(self.errs, "Mismatch:\n"..dump(cmp).."\n"..dump(p)) + table.insert(self.errs, + "Mismatch at packet #"..(self.seen+1)..":\n" + ..dump(cmp).."\n" + ..dump(p)) end else self.seen = self.seen + 1 diff --git a/src/lib/protocol/README.md b/src/lib/protocol/README.md index e2e18e6dfc..9e008af461 100644 --- a/src/lib/protocol/README.md +++ b/src/lib/protocol/README.md @@ -197,6 +197,14 @@ Computes and sets the IPv4 header checksum. Its called automatically by Predicate methods to test if *ip* is equal to the source or destination addresses individually. +— Method **ipv4:swap** + +Swaps the source and destination addresses. + +— Method **ipv4:is_fragment** + +Returns true if the header denotes an IP fragment and false otherwise. + — Function **ipv4:pton** *string* Returns the binary representation of IPv4 address denoted by *string*. diff --git a/src/lib/protocol/ipv4.lua b/src/lib/protocol/ipv4.lua index c89f5940fb..0f050ff540 100644 --- a/src/lib/protocol/ipv4.lua +++ b/src/lib/protocol/ipv4.lua @@ -8,6 +8,8 @@ local header = require("lib.protocol.header") local ipsum = require("lib.checksum").ipsum local htons, ntohs, htonl, ntohl = lib.htons, lib.ntohs, lib.htonl, lib.ntohl +local band, lshift, rshift, bnot = + bit.band, bit.lshift, bit.rshift, bit.bnot -- TODO: generalize local AF_INET = 2 @@ -31,10 +33,10 @@ local ipv4 = subClass(header) ipv4._name = "ipv4" ipv4._ulp = { class_map = { + [1] = "lib.protocol.icmp.header", [6] = "lib.protocol.tcp", [17] = "lib.protocol.udp", [47] = "lib.protocol.gre", - [58] = "lib.protocol.icmp.header", }, method = 'protocol' } ipv4:init( @@ -62,7 +64,7 @@ function ipv4:new (config) o:ihl(o:sizeof() / 4) o:dscp(config.dscp or 0) o:ecn(config.ecn or 0) - o:total_length(o:sizeof()) -- default to header only + o:total_length(config.total_length or o:sizeof()) -- default to header only o:id(config.id or 0) o:flags(config.flags or 0) o:frag_off(config.frag_off or 0) @@ -139,6 +141,10 @@ function ipv4:frag_off (frag_off) return lib.bitfield(16, self:header(), 'frag_off', 3, 13, frag_off) end +function ipv4:is_fragment () + return self:frag_off() ~= 0 or band(self:flags(), 0x01) == 1 +end + function ipv4:ttl (ttl) if ttl ~= nil then self:header().ttl = ttl @@ -147,6 +153,25 @@ function ipv4:ttl (ttl) end end +-- Adopted from lwAFTR with love +function ipv4:ttl_decrement () + local old_ttl = self:ttl() + local new_ttl = band(old_ttl - 1, 0xff) + self:ttl(new_ttl) + local chksum = bnot(ntohs(self:header().checksum)) + -- Now fix up the checksum. The ttl field is the first byte in the + -- 16-bit big-endian word, so the difference to the overall sum is + -- multiplied by 0xff. + chksum = chksum + lshift(new_ttl - old_ttl, 8) + -- Now do the one's complement 16-bit addition of the 16-bit words of + -- the checksum, which necessarily is a 32-bit value. Two carry + -- iterations will suffice. + chksum = band(chksum, 0xffff) + rshift(chksum, 16) + chksum = band(chksum, 0xffff) + rshift(chksum, 16) + self:header().checksum = htons(bnot(chksum)) + return new_ttl +end + function ipv4:protocol (protocol) if protocol ~= nil then self:header().protocol = protocol @@ -174,8 +199,12 @@ function ipv4:src (ip) end end +local function ip_eq (x, y) + return ffi.cast("uint32_t *", x)[0] == ffi.cast("uint32_t *", y)[0] +end + function ipv4:src_eq (ip) - return C.memcmp(ip, self:header().src_ip, ipv4_addr_t_size) == 0 + return ip_eq(ip, self:header().src_ip, ipv4_addr_t_size) end function ipv4:dst (ip) @@ -187,7 +216,7 @@ function ipv4:dst (ip) end function ipv4:dst_eq (ip) - return C.memcmp(ip, self:header().dst_ip, ipv4_addr_t_size) == 0 + return ip_eq(ip, self:header().dst_ip, ipv4_addr_t_size) end -- override the default equality method @@ -199,6 +228,14 @@ function ipv4:eq (other) self:src_eq(other:src()) and self:dst_eq(other:dst()) end +function ipv4:swap () + local tmp = ipv4_addr_t() + local h = self:header() + ffi.copy(tmp, h.src_ip, ipv4_addr_t_size) + ffi.copy(h.dst_ip, h.src_ip, ipv4_addr_t_size) + ffi.copy(h.src_ip, tmp, ipv4_addr_t_size) +end + -- Return a pseudo header for checksum calculation in a upper-layer -- protocol (e.g. icmp). Note that the payload length and next-header -- values in the pseudo-header refer to the effective upper-layer diff --git a/src/program/vita/README.config b/src/program/vita/README.config index 7129e6857b..740650a06b 100644 --- a/src/program/vita/README.config +++ b/src/program/vita/README.config @@ -14,7 +14,6 @@ CONFIGURATION SYNTAX interface:= macaddr ; [ vlan ; ] - [ mtu ; ] pciaddr ; route:= @@ -23,6 +22,7 @@ CONFIGURATION SYNTAX gw_ip4 ; preshared_key ; spi ; + [ private_mtu ; ] NOTES @@ -32,10 +32,10 @@ NOTES Vita nodes. Each interface is identified by a Linux PCI bus address, and assigned an - Ethernet (MAC) address. Optionally, the MTU can be specified in bytes, and a - IEEE 802.1Q VLAN Identifier can be set. Given that the underlying hardware - device supports VMDq, it is possible to pass the same PCI bus address for - both interfaces to have them share a single physical port. + Ethernet (MAC) address. Optionally, a IEEE 802.1Q VLAN Identifier can be set. + Given that the underlying hardware device supports VMDq, it is possible to + pass the same PCI bus address for both interfaces to have them share a single + physical port. Each route is given a unique, human readable identifier that must satisfy the pattern [a-zA-Z0-9_]+ (i.e., one or more alphanumeric ASCII and underscore @@ -48,13 +48,20 @@ NOTES associating encrypted traffic for a given route. Like the pre-shared key, the SPI must be the same for both ends of a route. + Optionally, the private MTU can be specified in bytes. The default and + maximum permitted value is 8937. Since Vita performs neither fragmentation + nor reassembly it may be necessary to adjust the next-hop MTU accordingly. + Note that packets leaving the public interface will have an added packet size + overhead due to encapsulation (up to 57 bytes for IPv4 and up to 77 bytes for + IPv6.) + While the default configuration should be generally applicable, the negotiation timeout and lifetime of Security Associations (SA) can be specified in seconds. EXAMPLE - private_interface { macaddr 52:54:00:00:00:00; mtu 1280; pciaddr 0c:00.0; } + private_interface { macaddr 52:54:00:00:00:00; pciaddr 0c:00.0; } public_interface { macaddr 52:54:00:00:00:FF; pciaddr 0c:00.1; } private_ip4 192.168.10.10; @@ -76,7 +83,9 @@ EXAMPLE net_cidr4 192.168.30.0/24; gw_ip4 203.0.113.3; preshared_key CF0BDD7A058BE55C12B7F2AA30D23FF01BDF8BE6571F2019ED7F7EBD3DA97B47; - spi 1002; + spi 1223; } + private_mtu 1280; + sa_ttl 86400; diff --git a/src/program/vita/conftest.snabb b/src/program/vita/conftest.snabb index f3f4a4fd45..7a079deadc 100755 --- a/src/program/vita/conftest.snabb +++ b/src/program/vita/conftest.snabb @@ -11,21 +11,26 @@ local S = require("syscall") local confpath = shm.root.."/"..shm.resolve("group/testconf") local function commit_conf (conf) - local f = assert(io.open(confpath, "w"), "Unable to open file: "..confpath) - yang.print_config_for_schema(vita.schemata['esp-gateway'], conf, f) - f:close() + vita.save_config(vita.schemata['esp-gateway'], confpath, conf) end +worker.set_exit_on_worker_death(true) + worker.start( "PublicRouterLoopback", ([[require("program.vita.vita").public_router_loopback_worker(%q)]]) :format(confpath) ) +worker.start( + "Exchange", + ([[require("program.vita.vita").exchange_worker(%q)]]) + :format(confpath) +) S.sleep(1) local conf0 = { - private_interface = { macaddr = "52:54:00:00:00:00" }, - public_interface = { macaddr = "52:54:00:00:00:FF" }, + private_interface = { pciaddr = "00:00.0", macaddr = "52:54:00:00:00:00" }, + public_interface = { pciaddr = "00:00.0", macaddr = "52:54:00:00:00:FF" }, private_ip4 = "192.168.10.1", public_ip4 = "203.0.113.1", private_nexthop_ip4 = "192.168.10.1", @@ -37,8 +42,8 @@ commit_conf(conf0) S.sleep(3) local conf1 = { - private_interface = { macaddr = "52:54:00:00:00:00" }, - public_interface = { macaddr = "52:54:00:00:00:FF" }, + private_interface = { pciaddr = "00:00.0", macaddr = "52:54:00:00:00:00" }, + public_interface = { pciaddr = "00:00.0", macaddr = "52:54:00:00:00:FF" }, private_ip4 = "192.168.10.1", public_ip4 = "203.0.113.1", private_nexthop_ip4 = "192.168.10.1", @@ -61,8 +66,8 @@ commit_conf(conf1) S.sleep(3) local conf2 = { - private_interface = { macaddr = "52:54:00:00:00:00" }, - public_interface = { macaddr = "52:54:00:00:00:FF" }, + private_interface = { pciaddr = "00:00.0", macaddr = "52:54:00:00:00:00" }, + public_interface = { pciaddr = "00:00.0", macaddr = "52:54:00:00:00:FF" }, private_ip4 = "192.168.10.1", public_ip4 = "203.0.113.1", private_nexthop_ip4 = "192.168.10.1", @@ -81,8 +86,8 @@ commit_conf(conf2) S.sleep(3) local conf3 = { - private_interface = { macaddr = "52:54:00:00:00:00" }, - public_interface = { macaddr = "52:54:00:00:00:FF" }, + private_interface = { pciaddr = "00:00.0", macaddr = "52:54:00:00:00:00" }, + public_interface = { pciaddr = "00:00.0", macaddr = "52:54:00:00:00:FF" }, private_ip4 = "192.168.10.1", public_ip4 = "203.0.113.1", private_nexthop_ip4 = "192.168.10.1", @@ -101,8 +106,8 @@ commit_conf(conf3) S.sleep(3) local conf4 = { - private_interface = { macaddr = "52:54:00:00:00:00" }, - public_interface = { macaddr = "52:54:00:00:00:FF" }, + private_interface = { pciaddr = "00:00.0", macaddr = "52:54:00:00:00:00" }, + public_interface = { pciaddr = "00:00.0", macaddr = "52:54:00:00:00:FF" }, private_ip4 = "192.168.10.1", public_ip4 = "203.0.113.2", private_nexthop_ip4 = "192.168.10.1", @@ -121,8 +126,8 @@ commit_conf(conf4) S.sleep(3) local conf5 = { - private_interface = { macaddr = "52:54:00:00:00:00" }, - public_interface = { macaddr = "52:54:00:00:00:FF" }, + private_interface = { pciaddr = "00:00.0", macaddr = "52:54:00:00:00:00" }, + public_interface = { pciaddr = "00:00.0", macaddr = "52:54:00:00:00:FF" }, private_ip4 = "192.168.10.1", public_ip4 = "203.0.113.2", private_nexthop_ip4 = "192.168.10.1", @@ -141,8 +146,8 @@ commit_conf(conf5) S.sleep(3) local conf6 = { - private_interface = { macaddr = "52:54:00:00:00:00" }, - public_interface = { macaddr = "52:54:00:00:00:FF" }, + private_interface = { pciaddr = "00:00.0", macaddr = "52:54:00:00:00:00" }, + public_interface = { pciaddr = "00:00.0", macaddr = "52:54:00:00:00:FF" }, private_ip4 = "192.168.10.1", public_ip4 = "203.0.113.2", private_nexthop_ip4 = "192.168.10.1", diff --git a/src/program/vita/dispatch.lua b/src/program/vita/dispatch.lua new file mode 100644 index 0000000000..dd82428dda --- /dev/null +++ b/src/program/vita/dispatch.lua @@ -0,0 +1,229 @@ +-- Use of this source code is governed by the GNU AGPL license; see COPYING. + +module(...,package.seeall) + +local exchange = require("program.vita.exchange") +local icmp = require("program.vita.icmp") +local counter = require("core.counter") +local ethernet = require("lib.protocol.ethernet") +local ipv4 = require("lib.protocol.ipv4") +local pf_match = require("pf.match") +local ffi = require("ffi") + + +PrivateDispatch = { + name = "PrivateDispatch", + config = { + node_ip4 = {required=true} + }, + shm = { + rxerrors = {counter}, + ethertype_errors = {counter}, + checksum_errors = {counter} + } +} + +function PrivateDispatch:new (conf) + local o = { + p_box = ffi.new("struct packet *[1]"), + ip4 = ipv4:new({}), + dispatch = pf_match.compile(([[match { + ip dst host %s and icmp => icmp4 + ip dst host %s => protocol4_unreachable + ip => forward4 + arp => arp + otherwise => reject_ethertype + }]]):format(conf.node_ip4, conf.node_ip4)) + } + return setmetatable(o, {__index=PrivateDispatch}) +end + +function PrivateDispatch:forward4 () + local p = packet.shiftleft(self.p_box[0], ethernet:sizeof()) + assert(self.ip4:new_from_mem(p.data, p.length)) + if self.ip4:checksum_ok() then + -- Strip datagram of any Ethernet frame padding before encapsulation. + local d = packet.resize(p, math.min(self.ip4:total_length(), p.length)) + link.transmit(self.output.forward4, d) + else + packet.free(p) + counter.add(self.shm.rxerrors) + counter.add(self.shm.checksum_errors) + end +end + +function PrivateDispatch:icmp4 () + local p = packet.shiftleft(self.p_box[0], ethernet:sizeof()) + link.transmit(self.output.icmp4, p) +end + +function PrivateDispatch:arp () + local p = packet.shiftleft(self.p_box[0], ethernet:sizeof()) + link.transmit(self.output.arp, p) +end + +function PrivateDispatch:protocol4_unreachable () + local p = packet.shiftleft(self.p_box[0], ethernet:sizeof()) + link.transmit(self.output.protocol4_unreachable, p) +end + +function PrivateDispatch:reject_ethertype () + packet.free(self.p_box[0]) + counter.add(self.shm.rxerrors) + counter.add(self.shm.ethertype_errors) +end + +function PrivateDispatch:push () + local input = self.input.input + while not link.empty(input) do + local p = link.receive(input) + self.p_box[0] = p + self:dispatch(p.data, p.length) + end +end + + +PublicDispatch = { + name = "PublicDispatch", + config = { + node_ip4 = {required=true} + }, + shm = { + rxerrors = {counter}, + ethertype_errors = {counter}, + protocol_errors = {counter}, + fragment_errors = {counter} + } +} + +function PublicDispatch:new (conf) + local o = { + p_box = ffi.new("struct packet *[1]"), + dispatch = pf_match.compile(([[match { + ip[6:2] & 0x3FFF != 0 => reject_fragment + ip proto esp => forward4 + ip proto %d => protocol + ip dst host %s and icmp => icmp4 + ip dst host %s => protocol4_unreachable + ip => reject_protocol + arp => arp + otherwise => reject_ethertype + }]]):format(exchange.PROTOCOL, conf.node_ip4, conf.node_ip4)) + } + return setmetatable(o, {__index=PublicDispatch}) +end + +function PublicDispatch:forward4 () + local p = packet.shiftleft(self.p_box[0], ethernet:sizeof() + ipv4:sizeof()) + -- NB: Ignore potential differences between IP datagram and Ethernet size + -- since the minimum ESP packet exceeds 60 bytes in payload. + link.transmit(self.output.forward4, p) +end + +function PublicDispatch:protocol () + local p = packet.shiftleft(self.p_box[0], ethernet:sizeof() + ipv4:sizeof()) + link.transmit(self.output.protocol, p) +end + +function PublicDispatch:icmp4 () + local p = packet.shiftleft(self.p_box[0], ethernet:sizeof()) + link.transmit(self.output.icmp4, p) +end + +function PublicDispatch:arp () + local p = packet.shiftleft(self.p_box[0], ethernet:sizeof()) + link.transmit(self.output.arp, p) +end + +function PublicDispatch:protocol4_unreachable () + local p = packet.shiftleft(self.p_box[0], ethernet:sizeof()) + link.transmit(self.output.protocol4_unreachable, p) +end + +function PublicDispatch:reject_fragment () + packet.free(self.p_box[0]) + counter.add(self.shm.rxerrors) + counter.add(self.shm.fragment_errors) +end + +function PublicDispatch:reject_protocol () + packet.free(self.p_box[0]) + counter.add(self.shm.rxerrors) + counter.add(self.shm.protocol_errors) +end + +function PublicDispatch:reject_ethertype () + packet.free(self.p_box[0]) + counter.add(self.shm.rxerrors) + counter.add(self.shm.ethertype_errors) +end + +function PublicDispatch:push () + local input = self.input.input + while not link.empty(input) do + local p = link.receive(input) + self.p_box[0] = p + self:dispatch(p.data, p.length) + end +end + + +InboundDispatch = { + name = "InboundDispatch", + config = { + node_ip4 = {required=true}, + }, + shm = { + protocol_errors = {counter} + } +} + +function InboundDispatch:new (conf) + local o = { + node_ip4n = ipv4:pton(conf.node_ip4), + ip4 = ipv4:new({}) + } + return setmetatable(o, {__index=InboundDispatch}) +end + +function InboundDispatch:forward4 (p) + link.transmit(self.output.forward4, p) +end + +function InboundDispatch:icmp4 (p) + link.transmit(self.output.icmp4, p) +end + +function InboundDispatch:protocol4_unreachable (p) + link.transmit(self.output.protocol4_unreachable, p) +end + +function InboundDispatch:reject_protocol (p) + packet.free(p) + counter.add(self.shm.protocol_errors) +end + +function InboundDispatch:dispatch (p) + local ip4 = self.ip4:new_from_mem(p.data, p.length) + if ip4 then + if ip4:dst_eq(self.node_ip4n) then + if ip4:protocol() == icmp.ICMP4.PROTOCOL then + self:icmp4(p) + else + self:protocol4_unreachable(p) + end + else + self:forward4(p) + end + else + self:reject_protocol(p) + end +end + +function InboundDispatch:push () + for _, input in ipairs(self.input) do + while not link.empty(input) do + self:dispatch(link.receive(input)) + end + end +end diff --git a/src/program/vita/exchange.lua b/src/program/vita/exchange.lua index 8fb4144ce4..d3c82017f6 100644 --- a/src/program/vita/exchange.lua +++ b/src/program/vita/exchange.lua @@ -261,7 +261,7 @@ function KeyManager:push () for _, route in ipairs(self.routes) do if route.protocol:reset_if_expired() == Protocol.code.expired then counter.add(self.shm.negotiations_expired) - audit:log("Negotiation expired for '"..route.id.."' (negotiation_ttl") + audit:log("Negotiation expired for '"..route.id.."' (negotiation_ttl)") end if route.status < status.ready then self:negotiate(route) diff --git a/src/program/vita/gentest.lua b/src/program/vita/gentest.lua index e10e6ac45f..8c6c47a803 100644 --- a/src/program/vita/gentest.lua +++ b/src/program/vita/gentest.lua @@ -38,10 +38,13 @@ function gen_packet (conf, route, size) local d = datagram:new(packet.resize(packet.allocate(), payload_size)) d:push(ipv4:new{ src = ipv4:pton(conf.private_nexthop_ip4), dst = ipv4:pton(conf.route_prefix.."."..route..".1"), + total_length = ipv4:sizeof() + payload_size, ttl = 64 }) d:push(ethernet:new{ dst = ethernet:pton(conf.private_mac), type = 0x0800 }) - return d:packet() + local p = d:packet() + -- Pad to minimum Ethernet frame size (excluding four octet CRC) + return packet.resize(p, math.max(60, p.length)) end function gen_packets (conf) diff --git a/src/program/vita/icmp.lua b/src/program/vita/icmp.lua new file mode 100644 index 0000000000..a4282085dc --- /dev/null +++ b/src/program/vita/icmp.lua @@ -0,0 +1,286 @@ +-- Use of this source code is governed by the Apache 2.0 license; see COPYING. + +module(..., package.seeall) + +local ipv4 = require("lib.protocol.ipv4") +local icmp = require("lib.protocol.icmp.header") +local counter = require("core.counter") +local lib = require("core.lib") +local ffi = require("ffi") +local min, max, floor = math.min, math.max, math.floor + +ICMP4 = { + name = "ICMP4", + config = { + node_ip4 = {required=true}, + nexthop_mtu = {}, + max_pps = {default=100} + }, + shm = { + rxerrors = {counter}, + protocol_errors = {counter}, + type_not_implemented_errors = {counter}, + code_not_implemented_errors = {counter}, + destination_unreachable = {counter}, + net_unreachable = {counter}, + host_unreachable = {counter}, + protocol_unreachable = {counter}, + port_unreachable = {counter}, + fragmentation_needed = {counter}, + source_route_failed = {counter}, + time_exceeded = {counter}, + transit_ttl_exceeded = {counter}, + fragment_reassembly_time_exceeded = {counter}, + parameter_problem = {counter}, + redirect = {counter}, + redirect_net = {counter}, + redirect_host = {counter}, + redirect_tos_net = {counter}, + redirect_tos_host = {counter}, + echo_request = {counter} + }, + PROTOCOL = 1, -- ICMP = 1 + payload_offset = ipv4:sizeof() + icmp:sizeof(), + handlers = {} +} + +ICMP4.payload_base_t = ffi.typeof([[struct { + uint8_t unused[4]; + uint8_t excerpt[28]; + } __attribute__((packed))]]) + +ICMP4.fragmentation_needed_t = ffi.typeof([[struct { + uint8_t unused[2]; + uint16_t nexthop_mtu; + } __attribute__((packed))]]) + +ICMP4.payload_t = ffi.typeof([[union { + $ base; + $ fragmentation_needed; + } __attribute__((packed))]], + ICMP4.payload_base_t, + ICMP4.fragmentation_needed_t) + +ICMP4.payload_ptr_t = ffi.typeof("$ *", ICMP4.payload_t) + +function ICMP4:new (conf) + local o = { + ip4 = ipv4:new({ + ttl = 64, + protocol = ICMP4.PROTOCOL, + src = ipv4:pton(conf.node_ip4) + }), + ip4_in = ipv4:new({}), + icmp = icmp:new(), + nexthop_mtu = conf.nexthop_mtu or 1024, + nexthop_mtu_configured = conf.nexthop_mtu ~= nil, + throttle = lib.throttle(1), + max_pps = conf.max_pps, + buckets = {rx = 0, tx = 0}, + num_buckets = 2, + logger = nil + } + for bucket, _ in pairs(o.buckets) do + o.buckets[bucket] = floor(o.max_pps / o.num_buckets) + end + return setmetatable(o, {__index=ICMP4}) +end + +function ICMP4:link () + if not self.logger then + self.logger = lib.logger_new({module = self.appname}) + end + if self.input.fragmentation_needed and not self.nexthop_mtu_configured then + self.logger:log(("WARNING, 'fragmentation_needed' link attached but nexthop_mtu not configured, defaulting to %d.") + :format(self.nexthop_mtu)) + end +end + +function ICMP4:log (p) + local payload = ffi.cast(ICMP4.payload_ptr_t, p.data + ICMP4.payload_offset) + local payload_length = p.length - ICMP4.payload_offset + local excerpt = ffi.string( + payload.base.excerpt, + min(payload_length - ffi.sizeof(payload.base.unused), + ffi.sizeof(payload.base.excerpt)) + ) + self.logger:log(("received message from %s [type %d code %d packet %s ...]") + :format(ipv4:ntop(self.ip4_in:src()), + self.icmp:type(), + self.icmp:code(), + lib.hexdump(excerpt))) +end + +-- Destination Unreachable +ICMP4.handlers[3] = function (self, p) + self:log(p) + packet.free(p) + counter.add(self.shm.destination_unreachable) + counter.add( + ({ [0] = self.shm.net_unreachable, + [1] = self.shm.host_unreachable, + [2] = self.shm.protocol_unreachable, + [3] = self.shm.port_unreachable, + [4] = self.shm.fragmentation_needed, + [5] = self.shm.source_route_failed }) + [self.icmp:code()] + or self.shm.code_not_implemented_errors + ) +end + +-- Time Exceeded +ICMP4.handlers[11] = function (self, p) + self:log(p) + packet.free(p) + counter.add(self.shm.time_exceeded) + counter.add( + ({ [0] = self.shm.transit_ttl_exceeded, + [1] = self.shm.fragment_reassembly_time_exceeded }) + [self.icmp:code()] + or self.shm.code_not_implemented_errors + ) +end + +-- Parameter Problem +ICMP4.handlers[12] = function (self, p) + self:log(p) + packet.free(p) + counter.add(self.shm.parameter_problem) +end + +-- Redirect +ICMP4.handlers[5] = function (self, p) + self:log(p) + packet.free(p) + counter.add(self.shm.redirect) + counter.add( + ({ [0] = self.shm.redirect_net, + [1] = self.shm.redirect_host, + [2] = self.shm.redirect_tos_net, + [3] = self.shm.redirect_tos_host }) + [self.icmp:code()] + or self.shm.code_not_implemented_errors + ) +end + +-- Echo +ICMP4.handlers[8] = function (self, p) + -- Copy payload. + local reply = packet.from_pointer(self:msg_payload(p)) + -- Prepend ICMP header. + self.icmp:type(0) + self.icmp:code(0) + self.icmp:checksum(reply.data, reply.length) + reply = packet.prepend(reply, self.icmp:header(), icmp:sizeof()) + -- Prepend IP header. + self.ip4:dst(self.ip4_in:src()) + self.ip4:total_length(reply.length + ipv4:sizeof()) + self.ip4:checksum() + reply = packet.prepend(reply, self.ip4:header(), ipv4:sizeof()) + -- Send reply. + link.transmit(self.output.output, reply) + packet.free(p) + counter.add(self.shm.echo_request) +end + +function ICMP4:msg_payload (p) + local payload = p.data + ICMP4.payload_offset + local declared_length = self.ip4_in:total_length() - ICMP4.payload_offset + local actual_length = p.length - ICMP4.payload_offset + local length = max(0, min(actual_length, declared_length)) + return payload, length +end + +function ICMP4:handle_msg (p) + -- Ensure packet is a valid ICMP message, and not a fragment. + if not self.ip4_in:new_from_mem(p.data, p.length) + or self.ip4_in:protocol() ~= ICMP4.PROTOCOL + or self.ip4_in:is_fragment() + or not self.icmp:new_from_mem(p.data + ipv4:sizeof(), + p.length - ipv4:sizeof()) + or not self.icmp:checksum_check(self:msg_payload(p)) + then + packet.free(p) + counter.add(self.shm.rxerrors) + counter.add(self.shm.protocol_errors) + return + end + -- Ensure we have a handler for ICMP type of packet. + local handler = self.handlers[self.icmp:type()] + if not handler then + packet.free(p) + counter.add(self.shm.rxerrors) + counter.add(self.shm.type_not_implemented_errors) + return + end + -- Handle incoming message. + handler(self, p) +end + +function ICMP4:send_msg (msgtype, code, p, opt) + opt = opt or {} + local msg = packet.resize(packet.allocate(), ffi.sizeof(ICMP4.payload_t)) + local payload = ffi.cast(ICMP4.payload_ptr_t, msg.data) + -- Set fields, copy packet excerpt. + if opt.nexthop_mtu then + payload.fragmentation_needed.nexthop_mtu = lib.htons(opt.nexthop_mtu) + end + local excerpt_length = min(p.length, ffi.sizeof(payload.base.excerpt)) + ffi.copy(payload.base.excerpt, p.data, excerpt_length) + -- Prepend ICMP header + self.icmp:type(msgtype) + self.icmp:code(code) + self.icmp:checksum(msg.data, msg.length) + msg = packet.prepend(msg, self.icmp:header(), icmp:sizeof()) + -- Prepend IP header. + assert(self.ip4_in:new_from_mem(p.data, p.length)) + self.ip4:dst(self.ip4_in:src()) + self.ip4:total_length(ipv4:sizeof() + msg.length) + self.ip4:checksum() + msg = packet.prepend(msg, self.ip4:header(), ipv4:sizeof()) + -- Send message, free packet. + link.transmit(self.output.output, msg) + packet.free(p) +end + +function ICMP4:rate_limit (bucket) + if self.throttle() then + self.buckets[bucket] = + min(floor(self.buckets[bucket] + self.max_pps / self.num_buckets), + self.max_pps) + end + if self.buckets[bucket] > 0 then + self.buckets[bucket] = self.buckets[bucket] - 1 + return true + end +end + +function ICMP4:push () + -- Process ingoing messages. + while not link.empty(self.input.input) and self:rate_limit('rx') do + self:handle_msg(link.receive(self.input.input)) + end + + -- Process outgoing messages. + if self.input.protocol_unreachable then + while not link.empty(self.input.protocol_unreachable) + and self:rate_limit('tx') do + self:send_msg(3, 2, link.receive(self.input.protocol_unreachable)) + end + end + if self.input.fragmentation_needed then + while not link.empty(self.input.fragmentation_needed) + and self:rate_limit('tx') do + self:send_msg(3, 4, link.receive(self.input.fragmentation_needed), { + nexthop_mtu = self.nexthop_mtu + }) + end + end + if self.input.transit_ttl_exceeded then + while not link.empty(self.input.transit_ttl_exceeded) + and self:rate_limit('tx') do + self:send_msg(11, 0, link.receive(self.input.transit_ttl_exceeded)) + end + end + -- ...remainder is NYI. +end diff --git a/src/program/vita/nexthop.lua b/src/program/vita/nexthop.lua index e31d6f2292..f2343fbffc 100644 --- a/src/program/vita/nexthop.lua +++ b/src/program/vita/nexthop.lua @@ -22,7 +22,6 @@ NextHop4 = { nexthop_ip4 = {required=true} }, shm = { - protocol_errors = {counter}, arp_requests = {counter}, arp_replies = {counter}, arp_errors = {counter}, @@ -68,7 +67,6 @@ function NextHop4:new (conf) -- Headers to parse o.arp = arp:new{} o.arp_ipv4 = arp_ipv4:new{} - o.ip4 = ipv4:new{} -- Initially, we don’t know the hardware address of our next hop o.connected = false @@ -100,14 +98,7 @@ function NextHop4:push () for _, input in ipairs(self.forward) do while not link.empty(input) do local p = link.receive(input) - local ip4 = self.ip4:new_from_mem(p.data, p.length) - if ip4 and ip4:ttl() > 0 then - ip4:ttl(ip4:ttl() - 1) - ip4:checksum() - link.transmit(output, self:encapsulate(p, 0x0800)) - else - counter.add(self.shm.protocol_errors) - end + link.transmit(output, self:encapsulate(p, 0x0800)) end end diff --git a/src/program/vita/route.lua b/src/program/vita/route.lua index 9a7b9f517c..a6e3c65074 100644 --- a/src/program/vita/route.lua +++ b/src/program/vita/route.lua @@ -5,14 +5,10 @@ module(...,package.seeall) local counter = require("core.counter") local ethernet = require("lib.protocol.ethernet") local ipv4 = require("lib.protocol.ipv4") -local arp = require("lib.protocol.arp") -local esp_header = require("lib.protocol.esp") -local esp = require("lib.ipsec.esp") -local exchange = require("program.vita.exchange") +local esp = require("lib.protocol.esp") local lpm = require("lib.lpm.lpm4_248").LPM4_248 local ctable = require("lib.ctable") local ffi = require("ffi") -local packet_buffer -- route := { net_cidr4=(CIDR4), gw_ip4=(IPv4), preshared_key=(KEY) } @@ -20,23 +16,21 @@ local packet_buffer PrivateRouter = { name = "PrivateRouter", config = { - routes = {required=true} + routes = {required=true}, + mtu = {required=true} }, shm = { rxerrors = {counter}, - ethertype_errors = {counter}, - protocol_errors = {counter}, - route_errors = {counter} + route_errors = {counter}, + mtu_errors = {counter} } } function PrivateRouter:new (conf) local o = { routes = {}, - eth = ethernet:new({}), - ip4 = ipv4:new({}), - fwd4_packets = packet_buffer(), - arp_packets = packet_buffer() + mtu = conf.mtu, + ip4 = ipv4:new({}) } for id, route in pairs(conf.routes) do o.routes[#o.routes+1] = { @@ -67,60 +61,25 @@ function PrivateRouter:link () self.routing_table4:build() end -function PrivateRouter:push () - local input = self.input.input - - local fwd4_packets, fwd4_cursor = self.fwd4_packets, 0 - local arp_packets, arp_cursor = self.arp_packets, 0 - while not link.empty(input) do - local p = link.receive(input) - local eth = self.eth:new_from_mem(p.data, p.length) - if eth and eth:type() == 0x0800 then -- IPv4 - fwd4_packets[fwd4_cursor] = packet.shiftleft(p, ethernet:sizeof()) - fwd4_cursor = fwd4_cursor + 1 - elseif eth and eth:type() == arp.ETHERTYPE then - arp_packets[arp_cursor] = packet.shiftleft(p, ethernet:sizeof()) - arp_cursor = arp_cursor + 1 - else - packet.free(p) - counter.add(self.shm.rxerrors) - counter.add(self.shm.ethertype_errors) - end - end - - local new_cursor = 0 - for i = 0, fwd4_cursor - 1 do - local p = fwd4_packets[i] - local ip4 = self.ip4:new_from_mem(p.data, ipv4:sizeof()) - if ip4 and ip4:checksum_ok() then - fwd4_packets[new_cursor] = p - new_cursor = new_cursor + 1 - else - packet.free(p) - counter.add(self.shm.rxerrors) - counter.add(self.shm.protocol_errors) - end - end - fwd4_cursor = new_cursor - - for i = 0, fwd4_cursor - 1 do - self:forward4(fwd4_packets[i]) - end - - for i = 0, arp_cursor - 1 do - link.transmit(self.output.arp, arp_packets[i]) - end -end - function PrivateRouter:find_route4 (dst) return self.routes[self.routing_table4:search_bytes(dst)] end -function PrivateRouter:forward4 (p) - self.ip4:new_from_mem(p.data, p.length) +function PrivateRouter:route (p) + assert(self.ip4:new_from_mem(p.data, p.length)) local route = self:find_route4(self.ip4:dst()) if route then - link.transmit(route.link, p) + if p.length + ethernet:sizeof() <= self.mtu then + link.transmit(route.link, p) + else + counter.add(self.shm.rxerrors) + counter.add(self.shm.mtu_errors) + if bit.band(self.ip4:flags(), 2) == 2 then -- Don’t fragment bit set? + link.transmit(self.output.fragmentation_needed, p) + else + packet.free(p) + end + end else packet.free(p) counter.add(self.shm.rxerrors) @@ -128,31 +87,32 @@ function PrivateRouter:forward4 (p) end end +function PrivateRouter:push () + local input = self.input.input + while not link.empty(input) do + self:route(link.receive(input)) + end + local control = self.input.control + while not link.empty(control) do + self:route(link.receive(control)) + end +end + PublicRouter = { name = "PublicRouter", config = { - routes = {required=true}, - node_ip4 = {required=true} + routes = {required=true} }, shm = { - rxerrors = {counter}, - ethertype_errors = {counter}, - protocol_errors = {counter}, - route_errors = {counter}, + route_errors = {counter} } } function PublicRouter:new (conf) local o = { routes = {}, - eth = ethernet:new({}), - ip4 = ipv4:new({}), - esp = esp_header:new({}), - ip4_packets = packet_buffer(), - fwd4_packets = packet_buffer(), - protocol_packets = packet_buffer(), - arp_packets = packet_buffer() + esp = esp:new({}) } for id, route in pairs(conf.routes) do o.routes[#o.routes+1] = { @@ -179,78 +139,23 @@ function PublicRouter:link () end end +function PublicRouter:find_route4 (spi) + local entry = self.routing_table4:lookup_ptr(spi) + return entry and self.routes[entry.value] +end + function PublicRouter:push () local input = self.input.input - local ip4_packets, ip4_cursor = self.ip4_packets, 0 - local arp_packets, arp_cursor = self.arp_packets, 0 while not link.empty(input) do local p = link.receive(input) - local eth = self.eth:new_from_mem(p.data, p.length) - if eth and eth:type() == 0x0800 then -- IPv4 - ip4_packets[ip4_cursor] = packet.shiftleft(p, ethernet:sizeof()) - ip4_cursor = ip4_cursor + 1 - elseif eth and eth:type() == arp.ETHERTYPE then - arp_packets[arp_cursor] = packet.shiftleft(p, ethernet:sizeof()) - arp_cursor = arp_cursor + 1 + assert(self.esp:new_from_mem(p.data, p.length)) + local route = self:find_route4(self.esp:spi()) + if route then + link.transmit(route.link, p) else packet.free(p) - counter.add(self.shm.rxerrors) - counter.add(self.shm.ethertype_errors) + counter.add(self.shm.route_errors) end end - - local fwd4_packets, fwd4_cursor = self.fwd4_packets, 0 - local protocol_packets, protocol_cursor = self.protocol_packets, 0 - for i = 0, ip4_cursor - 1 do - local p = ip4_packets[i] - local ip4 = self.ip4:new_from_mem(p.data, p.length) - and self.ip4:checksum_ok() - and self.ip4 - if ip4 and ip4:protocol() == esp.PROTOCOL then - fwd4_packets[fwd4_cursor] = packet.shiftleft(p, ipv4:sizeof()) - fwd4_cursor = fwd4_cursor + 1 - elseif ip4 and ip4:protocol() == exchange.PROTOCOL then - protocol_packets[protocol_cursor] = packet.shiftleft(p, ipv4:sizeof()) - protocol_cursor = protocol_cursor + 1 - else - packet.free(p) - counter.add(self.shm.rxerrors) - counter.add(self.shm.protocol_errors) - end - end - - for i = 0, fwd4_cursor - 1 do - self:forward4(fwd4_packets[i]) - end - - for i = 0, protocol_cursor - 1 do - link.transmit(self.output.protocol, protocol_packets[i]) - end - - for i = 0, arp_cursor - 1 do - link.transmit(self.output.arp, arp_packets[i]) - end -end - -function PublicRouter:find_route4 (spi) - local entry = self.routing_table4:lookup_ptr(spi) - return entry and self.routes[entry.value] -end - -function PublicRouter:forward4 (p) - local route = self.esp:new_from_mem(p.data, p.length) - and self:find_route4(self.esp:spi()) - if route then - link.transmit(route.link, p) - else - packet.free(p) - counter.add(self.shm.rxerrors) - counter.add(self.shm.route_errors) - end -end - - -function packet_buffer () - return ffi.new("struct packet *[?]", link.max) end diff --git a/src/program/vita/selftest-pcaps.snabb b/src/program/vita/selftest-pcaps.snabb new file mode 100755 index 0000000000..17eb635160 --- /dev/null +++ b/src/program/vita/selftest-pcaps.snabb @@ -0,0 +1,315 @@ +#!snabb snsh + +-- Use of this source code is governed by the GNU AGPL license; see COPYING. + +local pcap = require("lib.pcap.pcap") +local ethernet = require("lib.protocol.ethernet") +local ipv4 = require("lib.protocol.ipv4") +local icmp = require("lib.protocol.icmp.header") +local esp = require("lib.ipsec.esp") +local datagram = require("lib.protocol.datagram") +local ffi = require("ffi") + +-- Synopsis: +-- +-- sudo selftest-pcaps.snabb +-- +-- Source selftest-*-in.pcap with packets that exercise various corner cases in +-- Vita. Anything that’s not the happy path. + +PcapLog = {} + +function PcapLog:new (filename) + local o = {} + o.file = io.open(filename, "w") + pcap.write_file_header(o.file) + return setmetatable(o, {__index=PcapLog}) +end + +function PcapLog:write (p) + pcap.write_record(self.file, p.data, p.length) +end + +local private = PcapLog:new("program/vita/selftest-private-in.pcap") +local public = PcapLog:new("program/vita/selftest-public-in.pcap") + +local private_src = ipv4:pton("192.168.0.1") +local private_dst = ipv4:pton("192.168.10.1") +local public_src = ipv4:pton("203.0.0.1") +local public_dst = ipv4:pton("203.0.113.1") +local remote_dst = ipv4:pton("192.168.10.2") + +function icmp4 (conf) + local payload = conf.payload or packet.from_string("0000Hello, World!") + local length = conf.payload_length or payload.length + local msg = datagram:new(payload) + local icm = icmp:new(conf.type, conf.code) + icm:checksum(msg:payload(), conf.payload_length or payload.length) + icm:header().checksum = conf.icmp_checksum or icm:header().checksum + msg:push(icm) + local ip4 = ipv4:new{ + flags = conf.flags, + frag_off = conf.frag_off, + total_length = ipv4:sizeof() + icmp:sizeof() + length, + ttl = conf.ttl or 64, + protocol = conf.protocol or 1, + src = conf.src, + dst = conf.dst + } + ip4:header().checksum = conf.ipv4_checksum or ip4:header().checksum + msg:push(ip4) + msg:push(ethernet:new{type=0x0800}) + return msg:packet() +end + +local sa = esp.encrypt:new{ + aead = "aes-gcm-16-icv", + spi = 1001, + key = "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00", + salt = "00 00 00 00" +} + +local sa_bad_spi = esp.encrypt:new{ + aead = "aes-gcm-16-icv", + spi = 0, + key = "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00", + salt = "00 00 00 00" +} + +local sa_replay = esp.encrypt:new{ + aead = "aes-gcm-16-icv", + spi = 1001, + key = "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00", + salt = "00 00 00 00" +} + +function encap4 (payload, conf) + payload = (conf.sa or sa):encapsulate_tunnel( + packet.shiftleft(payload, ethernet:sizeof()), + conf.nh or 4 + ) + local d = datagram:new(payload) + d:push(ipv4:new{ + flags = conf.flags, + frag_off = conf.frag_off, + total_length = ipv4:sizeof() + (conf.length or payload.length), + ttl = conf.ttl or 64, + protocol = esp.PROTOCOL, + src = conf.src, + dst = conf.dst + }) + d:push(ethernet:new{type=0x0800}) + return d:packet() +end + +-- Echo request +private:write(icmp4{ + type = 8, + src = private_src, + dst = private_dst +}) +public:write(icmp4{ + type = 8, + src = public_src, + dst = public_dst +}) +-- Broken echo request (too short) +private:write(icmp4{ + type = 8, + src = private_src, + dst = private_dst, + payload_length = 10000 +}) +-- Broken echo request (too long) +private:write(icmp4{ + type = 8, + src = private_src, + dst = private_dst, + payload_length = 4 +}) +-- Fragmented echo requests +private:write(icmp4{ + type = 8, + flags = 0x01, + src = private_src, + dst = private_dst +}) +private:write(icmp4{ + type = 8, + frag_off = 17, + src = private_src, + dst = private_dst +}) +-- Echo reply +private:write(icmp4{ + type = 0, + src = private_src, + dst = private_dst +}) +-- Encapsulated echo request +public:write(encap4( + icmp4{ + type = 8, + src = remote_dst, + dst = private_dst + }, + { + src = public_src, + dst = public_dst + } +)) +-- Unreachable protocol (private/public/inbound) +private:write(icmp4{ + protocol = 42, + src = private_src, + dst = private_dst +}) +public:write(icmp4{ + type = 8, + protocol = 42, + src = public_src, + dst = public_dst +}) +public:write(encap4( + icmp4{ + protocol = 42, + src = remote_dst, + dst = private_dst + }, + { + src = public_src, + dst = public_dst + } +)) +-- Fragmentation needed +private:write(icmp4{ + payload = packet.resize(packet.allocate(), 8000), + src = private_src, + dst = remote_dst +}) +private:write(icmp4{ + payload = packet.resize(packet.allocate(), 8000), + flags = 0x02, + src = private_src, + dst = remote_dst +}) +-- TTL expired (private/inbound) +private:write(icmp4{ + type = 8, + src = private_src, + dst = remote_dst, + ttl = 0 +}) +public:write(encap4( + icmp4{ + type = 8, + src = remote_dst, + dst = remote_dst, + ttl = 0 + }, + { + src = public_src, + dst = public_dst + } +)) +-- Reject ESP fragments +public:write(encap4( + icmp4{ + type = 8, + src = remote_dst, + dst = remote_dst + }, + { + src = public_src, + dst = public_dst, + flags = 0x01 + } +)) +public:write(encap4( + icmp4{ + type = 8, + src = remote_dst, + dst = remote_dst + }, + { + src = public_src, + dst = public_dst, + frag_off= 17 + } +)) +-- Bogus SPI +public:write(encap4( + icmp4{ + type = 8, + src = remote_dst, + dst = private_dst + }, + { + sa = sa_bad_spi, + src = public_src, + dst = public_dst + } +)) +-- Bogus SeqNo +public:write(encap4( + icmp4{ + type = 8, + src = remote_dst, + dst = private_dst + }, + { + sa = sa_replay, + src = public_src, + dst = public_dst + } +)) +-- Bogus NextHeader +public:write(encap4( + icmp4{ + type = 8, + src = remote_dst, + dst = private_dst + }, + { + src = public_src, + dst = public_dst, + nh = 42 + } +)) +-- Bogus checksums +private:write(icmp4{ + type = 8, + src = private_src, + dst = remote_dst, + ipv4_checksum = 42 +}) +private:write(icmp4{ + type = 8, + src = private_src, + dst = private_dst, + icmp_checksum = 42 +}) +-- Various ICMP messages +local payload = packet.from_string("....0123456789012345678901234567") +for _, msgtype in ipairs({ + {type=3, codes={0,1,2,3,4,5,100}}, + {type=11, codes={0,1,100}}, + {type=12, codes={0,100}}, + {type=4, codes={0, 100}}, + {type=5, codes={0,1,2,3,100}}, + {type=13, codes={0,100}}, + {type=14, codes={0,100}}, + {type=15, codes={0,100}}, + {type=16, codes={0,100}}, + {type=100, codes={0}} +}) do + for _, code in ipairs(msgtype.codes) do + private:write(icmp4{ + payload = packet.clone(payload), + type = msgtype.type, + code = code, + src = private_src, + dst = private_dst + }) + end +end diff --git a/src/program/vita/selftest-private-in.pcap b/src/program/vita/selftest-private-in.pcap new file mode 100644 index 0000000000..3c5f33e32f Binary files /dev/null and b/src/program/vita/selftest-private-in.pcap differ diff --git a/src/program/vita/selftest-private-out.pcap b/src/program/vita/selftest-private-out.pcap new file mode 100644 index 0000000000..36d7521b1b Binary files /dev/null and b/src/program/vita/selftest-private-out.pcap differ diff --git a/src/program/vita/selftest-public-in.pcap b/src/program/vita/selftest-public-in.pcap new file mode 100644 index 0000000000..7e0ad98671 Binary files /dev/null and b/src/program/vita/selftest-public-in.pcap differ diff --git a/src/program/vita/selftest-public-out.pcap b/src/program/vita/selftest-public-out.pcap new file mode 100644 index 0000000000..c32889c00b Binary files /dev/null and b/src/program/vita/selftest-public-out.pcap differ diff --git a/src/program/vita/selftest.snabb b/src/program/vita/selftest.snabb new file mode 100755 index 0000000000..64f76b0189 --- /dev/null +++ b/src/program/vita/selftest.snabb @@ -0,0 +1,247 @@ +#!snabb snsh + +-- Use of this source code is governed by the GNU AGPL license; see COPYING. + +local vita = require("program.vita.vita") +local ARP = require("apps.ipv4.arp").ARP +local ethernet = require("lib.protocol.ethernet") +local ipv4 = require("lib.protocol.ipv4") +local basic_apps = require("apps.basic.basic_apps") +local pcap = require("apps.pcap.pcap") +local filter = require("apps.packet_filter.pcap_filter") +local match = require("apps.test.match") +local counter = require("core.counter") +local shm = require("core.shm") + +-- Synopsis: +-- +-- sudo selftest.snabb [regenerate] +-- +-- Basic event-sourced (selftest-*-in.pcap) test that exercises various +-- non-happy paths of Vita. Regenerates reference outputs (selftest-*-out.pcap) +-- when called with an argument. +-- +-- TODO: doesn’t exercise KeyManager yet. + +local regenerate_pcaps = main.parameters[1] + +local cfg = { + private_interface = { + pciaddr = "00:00.0", + macaddr = "52:54:00:00:00:00" + }, + public_interface = { + pciaddr = "00:00.0", + macaddr = "52:54:00:00:00:FF" + }, + private_ip4 = "192.168.10.1", + public_ip4 = "203.0.113.1", + private_nexthop_ip4 = "192.168.0.1", + public_nexthop_ip4 = "203.0.0.1", + private_mtu = 500, + route = { + loopback = { + net_cidr4 = "192.168.10.0/24", + gw_ip4 = "203.0.113.1", + preshared_key = string.rep("00", 32), + spi = 1001 + } + } +} + +local ephemeral_keys = { + esp = { + sa = { + loopback_outbound = { + aead = "aes-gcm-16-icv", + spi = 1001, + key = "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00", + salt = "00 00 00 00" + } + } + }, + dsp = { + sa = { + loopback_inbound = { + aead = "aes-gcm-16-icv", + spi = 1001, + key = "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00", + salt = "00 00 00 00", + auditing = true + } + } + } +} + +-- In order to allow the encapsulator/decapsulator apps to run in the same +-- process, we make sure they do not collide with the interlink app names of +-- the routing processes by using different route identifiers in their +-- ephemeral key configuration above, and alias them to the shared interlink +-- objects so they connect. +shm.alias("group/interlink/ESP_loopback_outbound_in.interlink", + "group/interlink/ESP_loopback_in.interlink") +shm.alias("group/interlink/DSP_loopback_inbound_in.interlink", + "group/interlink/DSP_loopback_in.interlink") +shm.alias("group/interlink/ESP_loopback_outbound_out.interlink", + "group/interlink/ESP_loopback_out.interlink") +shm.alias("group/interlink/DSP_loopback_inbound_out.interlink", + "group/interlink/DSP_loopback_out.interlink") + +local c = config.new() + +-- Configure Vita app network (all-in-one process.) +local _, private = vita.configure_private_router(cfg, c) +local _, public = vita.configure_public_router(cfg, c) +vita.configure_esp(ephemeral_keys.esp, c) +vita.configure_dsp(ephemeral_keys.dsp, c) + +-- Add ARP resolvers. +config.app(c, "private_arp", ARP, { + self_mac = ethernet:pton("52:54:00:00:00:00"), + self_ip = ipv4:pton("192.168.0.1"), + next_ip = ipv4:pton("192.168.10.1") +}) +config.app(c, "public_arp", ARP, { + self_mac = ethernet:pton("52:54:00:00:00:FE"), + self_ip = ipv4:pton("203.0.0.1"), + next_ip = ipv4:pton("203.0.113.1") +}) +config.link(c, "private_arp.south -> "..private.input) +config.link(c, private.output.." -> private_arp.south") +config.link(c, "public_arp.south -> "..public.input) +config.link(c, public.output.." -> public_arp.south") + +-- Loopback ESP traffic. +config.app(c, "public_out", basic_apps.Tee) +config.app(c, "join", basic_apps.Join) +config.app(c, "filter", filter.PcapFilter, {filter="ip proto esp"}) +config.link(c, "public_arp.north -> public_out.input") +config.link(c, "public_out.loopback -> filter.input") +config.link(c, "filter.output -> join.loopback") +config.link(c, "join.output -> public_arp.north") + +-- Add PCAP sources and sinks. +config.app(c, "private_pcap_in", pcap.PcapReader, + "program/vita/selftest-private-in.pcap") +config.app(c, "public_pcap_in", pcap.PcapReader, + "program/vita/selftest-public-in.pcap") +config.link(c, "private_pcap_in.output -> private_arp.north") +config.link(c, "public_pcap_in.output -> join.input") +if regenerate_pcaps then + -- Regenerate reference outputs. + config.app(c, "private_pcap_out", pcap.PcapWriter, + "program/vita/selftest-private-out.pcap") + config.app(c, "public_pcap_out", pcap.PcapWriter, + "program/vita/selftest-public-out.pcap") + config.link(c, "private_arp.north -> private_pcap_out.input") + config.link(c, "public_out.output -> public_pcap_out.input") +else + -- Match reference outputs. + config.app(c, "private_pcap_out", pcap.PcapReader, + "program/vita/selftest-private-out.pcap") + config.app(c, "public_pcap_out", pcap.PcapReader, + "program/vita/selftest-public-out.pcap") + config.app(c, "match_private", match.Match, {}) + config.link(c, "private_pcap_out.output -> match_private.comparator") + config.link(c, "private_arp.north -> match_private.rx") + config.app(c, "match_public", match.Match, {}) + config.link(c, "public_pcap_out.output -> match_public.comparator") + config.link(c, "public_out.output -> match_public.rx") +end + +engine.configure(c) + +-- Hack to avoid ESP seq# reuse because of packets from public_in.pcap +engine.app_table.ESP_loopback_outbound.sa.seq.no = 100 + +-- Run engine until its idle (all packets have been processed). +local last_frees = counter.read(engine.frees) +local function is_idle () + if counter.read(engine.frees) == last_frees then return true + else last_frees = counter.read(engine.frees) end +end +engine.main({done=is_idle}) + +if regenerate_pcaps then + -- Print final statistics. + engine.report_links() + for appname, app in pairs(engine.app_table) do + if app.shm then + print() + print(appname) + for name, _ in pairs(app.shm.specs) do + local value = counter.read(app.shm[name]) + if value > 0 then + print(("%00d %s"):format(tonumber(value), name)) + end + end + end + end +else + -- Assert application state is as expected. + if #engine.app_table.match_private:errors() > 0 then + engine.app_table.match_private:report() + main.exit(1) + end + if #engine.app_table.match_public:errors() > 0 then + engine.app_table.match_public:report() + main.exit(1) + end + for app, counters in pairs{ + PrivateRouter = { + rxerrors = 2, + mtu_errors = 2 + }, + PublicRouter = { + route_errors = 1, + }, + PrivateDispatch = { + rxerrors = 1, + checksum_errors = 1 + }, + PublicDispatch = { + rxerrors = 2, + fragment_errors = 2 + }, + DSP_loopback_inbound = { + rxerrors = 2, + protocol_errors = 1, + decrypt_errors = 1 + }, + PrivateICMP4 = { + rxerrors = 15, + protocol_errors = 3, + redirect = 5, + redirect_net = 1, + redirect_host = 1, + redirect_tos_net = 1, + redirect_tos_host = 1, + destination_unreachable = 7, + net_unreachable = 1, + protocol_unreachable = 1, + port_unreachable = 1, + host_unreachable = 1, + fragmentation_needed = 1, + source_route_failed = 1, + echo_request = 3, + time_exceeded = 3, + transit_ttl_exceeded = 1, + fragment_reassembly_time_exceeded = 1, + parameter_problem = 2, + type_not_implemented_errors = 12, + code_not_implemented_errors = 3 + }, + PublicICMP4 = { + echo_request = 1 + }, + InboundICMP4 = { + echo_request = 1 + } + } do + for name, should in pairs(counters) do + local actual = tonumber(counter.read(engine.app_table[app].shm[name])) + assert(should == actual, + name.." should be "..should.." but is "..actual) + end + end +end diff --git a/src/program/vita/test.snabb b/src/program/vita/test.snabb index 014718059b..4d93ae4eb1 100755 --- a/src/program/vita/test.snabb +++ b/src/program/vita/test.snabb @@ -93,11 +93,7 @@ engine.configure(c) worker.set_exit_on_worker_death(true) local confpath = shm.root.."/"..shm.resolve("group/testconf") -do - local f = assert(io.open(confpath, "w"), "Unable to open file: "..confpath) - yang.print_config_for_schema(vita.schemata['esp-gateway'], conf, f) - f:close() -end +vita.save_config(vita.schemata['esp-gateway'], confpath, conf) worker.start( "PublicRouterLoopback", diff --git a/src/program/vita/ttl.lua b/src/program/vita/ttl.lua new file mode 100644 index 0000000000..96bf7cd705 --- /dev/null +++ b/src/program/vita/ttl.lua @@ -0,0 +1,37 @@ +-- Use of this source code is governed by the GNU AGPL license; see COPYING. + +module(...,package.seeall) + +local counter = require("core.counter") +local ipv4 = require("lib.protocol.ipv4") + +DecrementTTL = { + name = "DecrementTTL", + shm = { + protocol_errors = {counter} + } +} + +function DecrementTTL:new () + return setmetatable({ip4 = ipv4:new({})}, {__index=DecrementTTL}) +end + +function DecrementTTL:push () + local output = self.output.output + local time_exceeded = self.output.time_exceeded + for _, input in ipairs(self.input) do + while not link.empty(input) do + local p = link.receive(input) + local ip4 = self.ip4:new_from_mem(p.data, p.length) + if ip4 and ip4:ttl() > 0 then + ip4:ttl_decrement() + link.transmit(output, p) + elseif ip4 then + link.transmit(time_exceeded, p) + else + packet.free(p) + counter.add(self.shm.protocol_errors) + end + end + end +end diff --git a/src/program/vita/tunnel.lua b/src/program/vita/tunnel.lua index 8a06742665..e317d3713b 100644 --- a/src/program/vita/tunnel.lua +++ b/src/program/vita/tunnel.lua @@ -98,6 +98,7 @@ function Tunnel4:new (conf) src = ipv4:pton(conf.src), dst = ipv4:pton(conf.dst), protocol = esp.PROTOCOL, + flags = 2, -- Don’t Fragment ttl = 64 }, ip = ipv4:new{} diff --git a/src/program/vita/vita-esp-gateway.yang b/src/program/vita/vita-esp-gateway.yang index f12349ee43..21cc936e56 100644 --- a/src/program/vita/vita-esp-gateway.yang +++ b/src/program/vita/vita-esp-gateway.yang @@ -18,7 +18,6 @@ module vita-esp-gateway { leaf pciaddr { type pci-address; mandatory true; } leaf macaddr { type yang:mac-address; mandatory true; } leaf vlan { type uint16 { range "0..4095"; } } - leaf mtu { type uint16; } } container private_interface { uses interface; } @@ -30,6 +29,8 @@ module vita-esp-gateway { leaf private_nexthop_ip4 { type inet:ipv4-address-no-zone; mandatory true; } leaf public_nexthop_ip4 { type inet:ipv4-address-no-zone; mandatory true; } + leaf private_mtu { type uint16 { range "0..8937"; } } + list route { key id; unique "net_cidr4 preshared_key spi"; leaf id { type string { pattern '[\w_]+'; } mandatory true; } diff --git a/src/program/vita/vita.lua b/src/program/vita/vita.lua index fd4946a70c..faa34d31a2 100644 --- a/src/program/vita/vita.lua +++ b/src/program/vita/vita.lua @@ -5,15 +5,19 @@ module(...,package.seeall) local lib = require("core.lib") local shm = require("core.shm") local worker = require("core.worker") +local dispatch = require("program.vita.dispatch") +local ttl = require("program.vita.ttl") local route = require("program.vita.route") local tunnel = require("program.vita.tunnel") local nexthop = require("program.vita.nexthop") local exchange = require("program.vita.exchange") +local icmp = require("program.vita.icmp") schemata = require("program.vita.schemata") local interlink = require("lib.interlink") local Receiver = require("apps.interlink.receiver") local Transmitter = require("apps.interlink.transmitter") local intel_mp = require("apps.intel_mp.intel_mp") +local ipv4 = require("lib.protocol.ipv4") local numa = require("lib.numa") local yang = require("lib.yang.yang") local S = require("syscall") @@ -27,6 +31,7 @@ local confspec = { public_ip4 = {required=true}, private_nexthop_ip4 = {required=true}, public_nexthop_ip4 = {required=true}, + private_mtu = {default=8937}, route = {required=true}, negotiation_ttl = {}, sa_ttl = {} @@ -97,13 +102,44 @@ function configure_private_router (conf, append) conf = lib.parse(conf, confspec) local c = append or config.new() - config.app(c, "PrivateRouter", route.PrivateRouter, {routes=conf.route}) + config.app(c, "PrivateDispatch", dispatch.PrivateDispatch, { + node_ip4 = conf.private_ip4 + }) + config.app(c, "OutboundTTL", ttl.DecrementTTL) + config.app(c, "PrivateRouter", route.PrivateRouter, { + routes = conf.route, + mtu = conf.private_mtu + }) + config.app(c, "PrivateICMP4", icmp.ICMP4, { + node_ip4 = conf.private_ip4, + nexthop_mtu = conf.private_mtu + }) + config.app(c, "InboundDispatch", dispatch.InboundDispatch, { + node_ip4 = conf.private_ip4 + }) + config.app(c, "InboundTTL", ttl.DecrementTTL) + config.app(c, "InboundICMP4", icmp.ICMP4, { + node_ip4 = conf.private_ip4 + }) config.app(c, "PrivateNextHop", nexthop.NextHop4, { node_mac = conf.private_interface.macaddr, node_ip4 = conf.private_ip4, nexthop_ip4 = conf.private_nexthop_ip4 }) - config.link(c, "PrivateRouter.arp -> PrivateNextHop.arp") + config.link(c, "PrivateDispatch.forward4 -> OutboundTTL.input") + config.link(c, "PrivateDispatch.icmp4 -> PrivateICMP4.input") + config.link(c, "PrivateDispatch.arp -> PrivateNextHop.arp") + config.link(c, "PrivateDispatch.protocol4_unreachable -> PrivateICMP4.protocol_unreachable") + config.link(c, "OutboundTTL.output -> PrivateRouter.input") + config.link(c, "OutboundTTL.time_exceeded -> PrivateICMP4.transit_ttl_exceeded") + config.link(c, "PrivateRouter.fragmentation_needed -> PrivateICMP4.fragmentation_needed") + config.link(c, "PrivateICMP4.output -> PrivateNextHop.icmp4") + config.link(c, "InboundDispatch.forward4 -> InboundTTL.input") + config.link(c, "InboundDispatch.icmp4 -> InboundICMP4.input") + config.link(c, "InboundDispatch.protocol4_unreachable -> InboundICMP4.protocol_unreachable") + config.link(c, "InboundTTL.output -> PrivateNextHop.forward") + config.link(c, "InboundTTL.time_exceeded -> InboundICMP4.transit_ttl_exceeded") + config.link(c, "InboundICMP4.output -> PrivateRouter.control") for id, route in pairs(conf.route) do local private_in = "PrivateRouter."..id @@ -111,14 +147,14 @@ function configure_private_router (conf, append) config.app(c, ESP_in, Transmitter) config.link(c, private_in.." -> "..ESP_in..".input") - local private_out = "PrivateNextHop."..id + local private_out = "InboundDispatch."..id local DSP_out = "DSP_"..id.."_out" config.app(c, DSP_out, Receiver) config.link(c, DSP_out..".output -> "..private_out) end local private_links = { - input = "PrivateRouter.input", + input = "PrivateDispatch.input", output = "PrivateNextHop.output" } return c, private_links @@ -128,8 +164,13 @@ function configure_public_router (conf, append) conf = lib.parse(conf, confspec) local c = append or config.new() + config.app(c, "PublicDispatch", dispatch.PublicDispatch, { + node_ip4 = conf.public_ip4 + }) config.app(c, "PublicRouter", route.PublicRouter, { - routes = conf.route, + routes = conf.route + }) + config.app(c, "PublicICMP4", icmp.ICMP4, { node_ip4 = conf.public_ip4 }) config.app(c, "PublicNextHop", nexthop.NextHop4, { @@ -137,11 +178,15 @@ function configure_public_router (conf, append) node_ip4 = conf.public_ip4, nexthop_ip4 = conf.public_nexthop_ip4 }) - config.link(c, "PublicRouter.arp -> PublicNextHop.arp") + config.link(c, "PublicDispatch.forward4 -> PublicRouter.input") + config.link(c, "PublicDispatch.icmp4 -> PublicICMP4.input") + config.link(c, "PublicDispatch.arp -> PublicNextHop.arp") + config.link(c, "PublicDispatch.protocol4_unreachable -> PublicICMP4.protocol_unreachable") + config.link(c, "PublicICMP4.output -> PublicNextHop.icmp4") config.app(c, "Protocol_in", Transmitter) config.app(c, "Protocol_out", Receiver) - config.link(c, "PublicRouter.protocol -> Protocol_in.input") + config.link(c, "PublicDispatch.protocol -> Protocol_in.input") config.link(c, "Protocol_out.output -> PublicNextHop.protocol") for id, route in pairs(conf.route) do @@ -161,7 +206,7 @@ function configure_public_router (conf, append) end local public_links = { - input = "PublicRouter.input", + input = "PublicDispatch.input", output = "PublicNextHop.output" } @@ -176,11 +221,6 @@ function configure_private_router_with_nic (conf, append) local c, private = configure_private_router(conf, append or config.new()) - -- Gracious limit for user defined MTU on private interface to avoid packet - -- payload overun due to ESP tunnel overhead. - conf.private_interface.mtu = - math.min(conf.private_interface.mtu or 8000, 8000) - conf.private_interface.vmdq = true config.app(c, "PrivateNIC", intel_mp.Intel, conf.private_interface) @@ -271,8 +311,8 @@ end -- ephemeral_keys := { =(SA), ... } (see exchange) -function configure_esp (ephemeral_keys) - local c = config.new() +function configure_esp (ephemeral_keys, append) + local c = append or config.new() for id, sa in pairs(ephemeral_keys.sa) do -- Configure interlink receiver/transmitter for inbound SA @@ -290,8 +330,8 @@ function configure_esp (ephemeral_keys) return c end -function configure_dsp (ephemeral_keys) - local c = config.new() +function configure_dsp (ephemeral_keys, append) + local c = append or config.new() for id, sa in pairs(ephemeral_keys.sa) do -- Configure interlink receiver/transmitter for outbound SA @@ -335,6 +375,12 @@ function load_config (schema, confpath) ) end +function save_config (schema, confpath, conf) + local f = assert(io.open(confpath, "w"), "Unable to open file: "..confpath) + yang.print_config_for_schema(schema, conf, f) + f:close() +end + function listen_confpath (schema, confpath, loader, interval) interval = interval or 1e9