diff --git a/.travis.yml b/.travis.yml index ae6ccc4223..2a088d9496 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,8 @@ before_script: script: - make -j - (cd src && sudo program/vita/selftest.snabb) + - (cd src && sudo program/vita/selftest6.snabb) - (cd src && sudo ./snabb snsh -t program.vita.exchange) - (cd src && sudo program/vita/conftest.snabb) - (cd src && sudo program/vita/test.snabb IMIX 1e6 3) + - (cd src && sudo program/vita/test6.snabb IMIX 1e6 3) diff --git a/src/apps/ipv6/README.md b/src/apps/ipv6/README.md index df1636fc90..b4bf5db12b 100644 --- a/src/apps/ipv6/README.md +++ b/src/apps/ipv6/README.md @@ -6,19 +6,18 @@ The `nd_light` app implements a small subset of IPv6 neighbor discovery (RFC4861). It has two duplex ports, `north` and `south`. The `south` port attaches to a network on which neighbor discovery (ND) must be performed. The `north` port attaches to an app that processes IPv6 -packets (including full ethernet frames). Packets transmitted to the +packets (without ethernet headers). Packets transmitted to the `north` port must be wrapped in full Ethernet frames (which may be empty). -The `nd_light` app replies to neighbor solicitations for which it is -configured as a target and performs rudimentary address resolution for -its configured *next-hop* address. If address resolution succeeds, the -Ethernet headers of packets from the `north` port will be overwritten -with headers containing the discovered destination address and the -configured source address before they are transmitted over the `south` -port. All packets from the `north` port are discarded as long as ND has -not yet succeeded. Packets received from the `south` port are transmitted -to the `north` port unaltered. +The `nd_light` app replies to neighbor solicitations for which it is configured +as a target and performs rudimentary address resolution for its configured +*next-hop* address. If address resolution succeeds, the Ethernet headers +containing the discovered destination address and the configured source address +are prepended to packets received from the `north` port before they are +transmitted over the `south` port. All packets from the `north` port are +discarded as long as ND has not yet succeeded. Packets received from the +`south` port are transmitted to the `north` port unaltered. DIAGRAM: nd_light +----------+ diff --git a/src/apps/ipv6/nd_light.lua b/src/apps/ipv6/nd_light.lua index fc1febb6a8..45006addf6 100644 --- a/src/apps/ipv6/nd_light.lua +++ b/src/apps/ipv6/nd_light.lua @@ -62,8 +62,6 @@ nd_light.config = { nd_light.shm = { status = {counter, 2}, -- Link down rxerrors = {counter}, - txerrors = {counter}, - txdrop = {counter}, ns_checksum_errors = {counter}, ns_target_address_errors = {counter}, na_duplicate_errors = {counter}, @@ -362,23 +360,16 @@ function nd_light:push () l_in = self.input.north l_out = self.output.south + -- Do not forward packets from north until ND for the next-hop has + -- completed. + if not self._eth_header then + return + end while not link.empty(l_in) do - if not self._eth_header then - -- Drop packets until ND for the next-hop - -- has completed. - packet.free(link.receive(l_in)) - counter.add(self.shm.txdrop) - else - local p = cache.p - p[0] = link.receive(l_in) - if p[0].length >= self._eth_header:sizeof() then - self._eth_header:copy(p[0].data) - link.transmit(l_out, p[0]) - else - packet.free(p[0]) - counter.add(self.shm.txerrors) - end - end + local p = packet.prepend( + link.receive(l_in), self._eth_header:header(), ethernet:sizeof() + ) + link.transmit(l_out, p) end end diff --git a/src/lib/protocol/ipv6.lua b/src/lib/protocol/ipv6.lua index f1edcdde09..91ec1dde5d 100644 --- a/src/lib/protocol/ipv6.lua +++ b/src/lib/protocol/ipv6.lua @@ -66,6 +66,7 @@ function ipv6:new (config) o:version(6) o:traffic_class(config.traffic_class or defaults.traffic_class) o:flow_label(config.flow_label or defaults.flow_label) + o:payload_length(config.payload_length or 0) o:next_header(config.next_header or defaults.next_header) o:hop_limit(config.hop_limit or defaults.hop_limit) o:src(config.src) diff --git a/src/program/vita/README.config b/src/program/vita/README.config index 6d67017f6f..dc284949fb 100644 --- a/src/program/vita/README.config +++ b/src/program/vita/README.config @@ -1,9 +1,10 @@ CONFIGURATION SYNTAX configuration:= - private-interface { } - public-interface { } - [ route { } ]* + private-interface4 { } + public-interface4|public-interface6 { } + [ route4|route46 { } ]* + [ mtu ; ] [ negotiation-ttl ; ] [ sa-ttl ; ] [ data-pane ; ] @@ -12,18 +13,17 @@ CONFIGURATION SYNTAX interface:= pci ; - ip4 ; - nexthop-ip4 ; + ip4|ip6 ; + nexthop-ip4 ; [ mac ; ] [ nexthop-mac ; ] route:= id ; - net-cidr4 ; - gw-ip4 ; + net-cidr4 ; + gw-ip4|gw-ip6 ; preshared-key ; spi ; - [ private-mtu ; ] sa:= route ; @@ -34,19 +34,19 @@ CONFIGURATION SYNTAX NOTES - A Vita configuration defines the a private and a public network interface for - use by the gateway, a set of routes to other Vita nodes, and the gateway’s - MTU among other miscellaneous operational parameters. + A Vita configuration defines a private and a public network interface for use + by the gateway, a set of routes to other Vita nodes, and the gateway’s MTU + among other miscellaneous operational parameters. Each interface is identified by a Linux PCI bus address, and assigned an - IPv4 address as well as the IPv4 address of the next hop through which all + IP address as well as the IP address of the next hop through which all packets leaving the interface will be routed. The IP addresses assigned to both interfaces may be the same. If the Ethernet (MAC) address of the interface is not explicitly specified, - it is automatically derived from its assigned IPv4 address by prepending the - bytes 2a:bb: or 3a:bb: depending on whether it is the private or public - interface respectively. + it is automatically derived from its assigned IP address by prepending the + bytes 2a:bb: or 3a:bb: to its least significant 32 bits depending on whether + it is the private or public interface respectively. Given that the NIC driver in use supports VMDq, it is possible to pass the same PCI bus address for both interfaces to have them share a single physical @@ -58,11 +58,10 @@ NOTES 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 - characters.) The route’s destination IPv4 subnet and gateway are specified - with an IPv4 prefix in CIDR notation, and an IPv4 address respectively. - Packets that arrive on the private interface and which are destined to a - route’s specified IPv4 subnetwork are tunneled to the destination gateway of - that route. + characters.) The route’s destination IP subnet and gateway are specified with + an IP prefix in CIDR notation, and an IP address respectively. Packets that + arrive on the private interface and which are destined to a route’s specified + IP subnetwork are tunneled to the destination gateway of that route. For authentication, each route is assigned a unique, pre-shared 256-bit key, encoded as a hexadecimal string (two digits for each octet, most significant @@ -94,21 +93,26 @@ NOTES 32-bit salt respectively, encoded as hexadecimal strings (two digits for each octet, most significant digit first). +REFERENCES + + Refer to vita-esp-gateway.yang for a formal YANG model that defines Vita’s + configuration as well as the statistics counters queryable at runtime. + EXAMPLE - private-interface { + private-interface4 { pci 0c:00.0; ip4 172.16.1.10; nexthop-ip4 172.16.1.1; } - public-interface { + public-interface4 { pci 0c:00.1; ip4 203.0.113.10; nexthop-ip4 172.16.1.1; } - route { + route4 { id site2; net-cidr4 172.16.2.0/24; gw-ip4 203.0.113.2; @@ -116,7 +120,7 @@ EXAMPLE spi 1001; } - route { + route4 { id site3; net-cidr4 172.16.3.0/24; gw-ip4 203.0.113.3; diff --git a/src/program/vita/conftest.snabb b/src/program/vita/conftest.snabb index fe1b97ab3d..ed43642419 100755 --- a/src/program/vita/conftest.snabb +++ b/src/program/vita/conftest.snabb @@ -26,22 +26,22 @@ test_conf(vita_test.gen_configuration({nroutes=1})) print("Change route key...") local conf = vita_test.gen_configuration({nroutes=1}) -conf.route.test1.preshared_key = string.rep("FF", 32) +conf.route4.test1.preshared_key = string.rep("FF", 32) test_conf(conf) print("Change route gw_ip4...") -conf.route.test1.gw_ip4 = "172.17.1.10" +conf.route4.test1.gw_ip4 = "172.17.1.10" test_conf(conf) print("Change public_ip4 and route net_cidr4...") -conf.public_interface.ip4 = "172.16.0.11" -conf.public_interface.nexthop_ip4 = conf.public_interface.ip4 -conf.route.test1.net_cidr4 = "172.16.0.0/16" +conf.public_interface4.ip4 = "172.16.0.11" +conf.public_interface4.nexthop_ip4 = conf.public_interface4.ip4 +conf.route4.test1.net_cidr4 = "172.16.0.0/16" test_conf(conf) print("Change route id...") -conf.route.test2 = conf.route.test1 -conf.route.test1 = nil +conf.route4.test2 = conf.route4.test1 +conf.route4.test1 = nil test_conf(conf) print("Change negotiation_ttl...") @@ -53,5 +53,5 @@ conf.sa_ttl = 42 test_conf(conf) print("Remove route...") -conf.route.test2 = nil +conf.route4.test2 = nil test_conf(conf) diff --git a/src/program/vita/dispatch.lua b/src/program/vita/dispatch.lua index dd82428dda..1d2036a462 100644 --- a/src/program/vita/dispatch.lua +++ b/src/program/vita/dispatch.lua @@ -7,6 +7,7 @@ local icmp = require("program.vita.icmp") local counter = require("core.counter") local ethernet = require("lib.protocol.ethernet") local ipv4 = require("lib.protocol.ipv4") +local ipv6 = require("lib.protocol.ipv6") local pf_match = require("pf.match") local ffi = require("ffi") @@ -86,7 +87,8 @@ end PublicDispatch = { name = "PublicDispatch", config = { - node_ip4 = {required=true} + node_ip4 = {}, + node_ip6 = {} }, shm = { rxerrors = {counter}, @@ -98,8 +100,10 @@ PublicDispatch = { function PublicDispatch:new (conf) local o = { - p_box = ffi.new("struct packet *[1]"), - dispatch = pf_match.compile(([[match { + p_box = ffi.new("struct packet *[1]") + } + if conf.node_ip4 then + o.dispatch = pf_match.compile(([[match { ip[6:2] & 0x3FFF != 0 => reject_fragment ip proto esp => forward4 ip proto %d => protocol @@ -109,7 +113,17 @@ function PublicDispatch:new (conf) arp => arp otherwise => reject_ethertype }]]):format(exchange.PROTOCOL, conf.node_ip4, conf.node_ip4)) - } + elseif conf.node_ip6 then + o.dispatch = pf_match.compile(([[match { + ip6 proto esp => forward6 + ip6 proto %d => protocol6 + ip6 and icmp6 and (ip6[40] = 135 or ip6[40] = 136) => nd + ip6 dst host %s and icmp6 => icmp6 + ip6 dst host %s => protocol6_unreachable + ip6 => reject_protocol + otherwise => reject_ethertype + }]]):format(exchange.PROTOCOL, conf.node_ip6, conf.node_ip6, conf.node_ip6)) + else error("Need either node_ip4 or node_ip6.") end return setmetatable(o, {__index=PublicDispatch}) end @@ -120,26 +134,52 @@ function PublicDispatch:forward4 () link.transmit(self.output.forward4, p) end +function PublicDispatch:forward6 () + local p = packet.shiftleft(self.p_box[0], ethernet:sizeof() + ipv6: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.forward6, 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:protocol6 () + local p = packet.shiftleft(self.p_box[0], ethernet:sizeof() + ipv6: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:icmp6 () + local p = packet.shiftleft(self.p_box[0], ethernet:sizeof()) + link.transmit(self.output.icmp6, p) +end + function PublicDispatch:arp () local p = packet.shiftleft(self.p_box[0], ethernet:sizeof()) link.transmit(self.output.arp, p) end +function PublicDispatch:nd () + link.transmit(self.output.nd, self.p_box[0]) +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:protocol6_unreachable () + local p = packet.shiftleft(self.p_box[0], ethernet:sizeof()) + link.transmit(self.output.protocol6_unreachable, p) +end + function PublicDispatch:reject_fragment () packet.free(self.p_box[0]) counter.add(self.shm.rxerrors) diff --git a/src/program/vita/exchange.lua b/src/program/vita/exchange.lua index 3239ad94a2..dce19adfb2 100644 --- a/src/program/vita/exchange.lua +++ b/src/program/vita/exchange.lua @@ -153,6 +153,7 @@ local counter = require("core.counter") local header = require("lib.protocol.header") local lib = require("core.lib") local ipv4 = require("lib.protocol.ipv4") +local ipv6 = require("lib.protocol.ipv6") local yang = require("lib.yang.yang") local schemata = require("program.vita.schemata") local audit = lib.logger_new({rate=32, module='KeyManager'}) @@ -164,7 +165,8 @@ PROTOCOL = 99 -- “Any private encryption scheme” KeyManager = { name = "KeyManager", config = { - node_ip4 = {required=true}, + node_ip4 = {}, + node_ip6 = {}, routes = {required=true}, sa_db_path = {required=true}, num_outbound_sa = {default=1}, @@ -193,7 +195,8 @@ KeyManager = { function KeyManager:new (conf) local o = { routes = {}, - ip = ipv4:new({}), + ip4 = ipv4:new({}), + ip6 = ipv6:new({}), transport = Transport.header:new({}), nonce_message = Protocol.nonce_message:new({}), key_message = Protocol.key_message:new({}), @@ -255,9 +258,11 @@ function KeyManager:reconfig (conf) end else -- insert new new route + assert(route.gw_ip4 or route.gw_ip6, "Need either gw_ip4 or gw_ip6") local new_route = { id = id, - gw_ip4n = ipv4:pton(route.gw_ip4), + gw_ip4n = route.gw_ip4 and ipv4:pton(route.gw_ip4), + gw_ip6n = route.gw_ip6 and ipv6:pton(route.gw_ip6), preshared_key = new_key, spi = route.spi, inbound_sa = {}, outbound_sa = {}, outbound_sa_queue = {}, @@ -282,7 +287,9 @@ function KeyManager:reconfig (conf) end -- switch to new configuration - self.node_ip4n = ipv4:pton(conf.node_ip4) + assert(conf.node_ip4 or conf.node_ip6, "Need either node_ip4 or node_ip6") + self.node_ip4n = conf.node_ip4 and ipv4:pton(conf.node_ip4) + self.node_ip6n = conf.node_ip6 and ipv6:pton(conf.node_ip6) self.routes = new_routes self.sa_db_file = shm.root.."/"..shm.resolve(conf.sa_db_path) self.num_outbound_sa = conf.num_outbound_sa @@ -472,7 +479,8 @@ function KeyManager:handle_nonce_key_request (route, message) -- This is an optimization for loopback testing: if we are negotiating with -- ourselves, configure an inbound SA only (outbound SA will be configured -- by the initiator.) - local is_loopback = self.ip:src_eq(route.gw_ip4n) + local is_loopback = (route.gw_ip4n and self.ip4:src_eq(route.gw_ip4n)) or + (route.gw_ip6n and self.ip6:src_eq(route.gw_ip6n)) counter.add(self.shm.keypairs_offered) audit:log(("Offered key pair for '%s' (inbound SA %d, outbound SA %s)"): @@ -509,7 +517,8 @@ function KeyManager:handle_key_request (route, message) -- This is an optimization for loopback testing: if we are negotiating with -- ourselves, configure an outbound SA only (inbound SA has been configured -- by the responder.) - local is_loopback = self.ip:src_eq(route.gw_ip4n) + local is_loopback = (route.gw_ip4n and self.ip4:src_eq(route.gw_ip4n)) or + (route.gw_ip6n and self.ip6:src_eq(route.gw_ip6n)) counter.add(self.shm.keypairs_negotiated) audit:log(("Completed AKE for '%s' (inbound SA %s, outbound SA %d)"): @@ -585,16 +594,27 @@ end function KeyManager:request (route, message) local request = packet.allocate() - self.ip:new({ - total_length = ipv4:sizeof() - + Transport.header:sizeof() - + message:sizeof(), - ttl = 64, - protocol = PROTOCOL, - src = self.node_ip4n, - dst = route.gw_ip4n - }) - packet.append(request, self.ip:header(), ipv4:sizeof()) + if self.node_ip4n then + self.ip4:new({ + total_length = ipv4:sizeof() + + Transport.header:sizeof() + + message:sizeof(), + ttl = 64, + protocol = PROTOCOL, + src = self.node_ip4n, + dst = route.gw_ip4n + }) + packet.append(request, self.ip4:header(), ipv4:sizeof()) + elseif self.node_ip6n then + self.ip6:new({ + payload_length = Transport.header:sizeof() + message:sizeof(), + hop_limit = 64, + next_header = PROTOCOL, + src = self.node_ip6n, + dst = route.gw_ip6n + }) + packet.append(request, self.ip6:header(), ipv6:sizeof()) + else error("BUG") end self.transport:new({ spi = route.spi, diff --git a/src/program/vita/icmp.lua b/src/program/vita/icmp.lua index a4282085dc..8cbcbdb19b 100644 --- a/src/program/vita/icmp.lua +++ b/src/program/vita/icmp.lua @@ -3,6 +3,7 @@ module(..., package.seeall) local ipv4 = require("lib.protocol.ipv4") +local ipv6 = require("lib.protocol.ipv6") local icmp = require("lib.protocol.icmp.header") local counter = require("core.counter") local lib = require("core.lib") @@ -71,6 +72,7 @@ function ICMP4:new (conf) src = ipv4:pton(conf.node_ip4) }), ip4_in = ipv4:new({}), + icmp_in = icmp:new(), icmp = icmp:new(), nexthop_mtu = conf.nexthop_mtu or 1024, nexthop_mtu_configured = conf.nexthop_mtu ~= nil, @@ -106,8 +108,8 @@ function ICMP4:log (p) ) 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(), + self.icmp_in:type(), + self.icmp_in:code(), lib.hexdump(excerpt))) end @@ -123,7 +125,7 @@ ICMP4.handlers[3] = function (self, p) [3] = self.shm.port_unreachable, [4] = self.shm.fragmentation_needed, [5] = self.shm.source_route_failed }) - [self.icmp:code()] + [self.icmp_in:code()] or self.shm.code_not_implemented_errors ) end @@ -136,7 +138,7 @@ ICMP4.handlers[11] = function (self, p) counter.add( ({ [0] = self.shm.transit_ttl_exceeded, [1] = self.shm.fragment_reassembly_time_exceeded }) - [self.icmp:code()] + [self.icmp_in:code()] or self.shm.code_not_implemented_errors ) end @@ -158,7 +160,7 @@ ICMP4.handlers[5] = function (self, p) [1] = self.shm.redirect_host, [2] = self.shm.redirect_tos_net, [3] = self.shm.redirect_tos_host }) - [self.icmp:code()] + [self.icmp_in:code()] or self.shm.code_not_implemented_errors ) end @@ -196,9 +198,9 @@ function ICMP4:handle_msg (p) 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)) + or not self.icmp_in:new_from_mem(p.data + ipv4:sizeof(), + p.length - ipv4:sizeof()) + or not self.icmp_in:checksum_check(self:msg_payload(p)) then packet.free(p) counter.add(self.shm.rxerrors) @@ -206,7 +208,7 @@ function ICMP4:handle_msg (p) return end -- Ensure we have a handler for ICMP type of packet. - local handler = self.handlers[self.icmp:type()] + local handler = self.handlers[self.icmp_in:type()] if not handler then packet.free(p) counter.add(self.shm.rxerrors) @@ -284,3 +286,301 @@ function ICMP4:push () end -- ...remainder is NYI. end + + +ICMP6 = { + name = "ICMP6", + config = { + node_ip6 = ICMP4.config.node_ip4, + nexthop_mtu = ICMP4.config.nexthop_mtu, + max_pps = ICMP4.config.max_pps + }, + shm = { + rxerrors = {counter}, + protocol_errors = {counter}, + type_not_implemented_errors = {counter}, + code_not_implemented_errors = {counter}, + destination_unreachable = {counter}, + net_unreachable = {counter}, + destination_denied = {counter}, + scope_denied = {counter}, + host_unreachable = {counter}, + port_unreachable = {counter}, + source_denied = {counter}, + net_denied = {counter}, + packet_too_big = {counter}, + time_exceeded = {counter}, + transit_hop_limit_exceeded = {counter}, + fragment_reassembly_time_exceeded = {counter}, + parameter_problem = {counter}, + header_field_problem = {counter}, + next_header_problem = {counter}, + option_problem = {counter}, + echo_request = {counter} + }, + PROTOCOL = 58, -- ICMPv6 = 58 + MIN_MTU = 1280, -- IPv6 minimum MTU + payload_offset = ipv6:sizeof() + icmp:sizeof(), + handlers = {} +} + +ICMP6.payload_base_t = ffi.typeof([[struct { + uint8_t unused[4]; + uint8_t excerpt[40]; + } __attribute__((packed))]]) + +ICMP6.packet_too_big_t = ffi.typeof([[struct { + uint32_t nexthop_mtu; + } __attribute__((packed))]]) + +ICMP6.parameter_problem_t = ffi.typeof([[struct { + uint32_t pointer; + } __attribute__((packed))]]) + +ICMP6.payload_t = ffi.typeof([[union { + $ base; + $ packet_too_big; + $ parameter_problem; + } __attribute__((packed))]], + ICMP6.payload_base_t, + ICMP6.packet_too_big_t, + ICMP6.parameter_problem_t) + +ICMP6.payload_ptr_t = ffi.typeof("$ *", ICMP6.payload_t) + +function ICMP6:new (conf) + local o = { + ip6 = ipv6:new({ + hop_limit = 64, + next_header = ICMP6.PROTOCOL, + src = ipv6:pton(conf.node_ip6) + }), + ip6_in = ipv6:new({}), + icmp_in = icmp:new(), + icmp = icmp:new(), + nexthop_mtu = conf.nexthop_mtu or 1280, + 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=ICMP6}) +end + +function ICMP6:link () + if not self.logger then + self.logger = lib.logger_new({module = self.appname}) + end + -- input link aliases + self.input.packet_too_big = + self.input.packet_too_big or self.input.fragmentation_needed + self.input.transit_hop_limit_exceeded = + self.input.transit_hop_limit_exceeded or self.input.transit_ttl_exceeded + -- a helpful warning + if self.input.packet_too_big and not self.nexthop_mtu_configured then + self.logger:log(("WARNING, 'packet_too_big' or 'fragmentation_needed' link attached but nexthop_mtu not configured, defaulting to %d.") + :format(self.nexthop_mtu)) + end +end + +function ICMP6:log (p) + local payload = ffi.cast(ICMP6.payload_ptr_t, p.data + ICMP6.payload_offset) + local payload_length = p.length - ICMP6.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(ipv6:ntop(self.ip6_in:src()), + self.icmp_in:type(), + self.icmp_in:code(), + lib.hexdump(excerpt))) +end + +-- Destination Unreachable +ICMP6.handlers[1] = 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.destination_denied, + [2] = self.shm.scope_denied, + [3] = self.shm.host_unreachable, + [4] = self.shm.port_unreachable, + [5] = self.shm.source_denied, + [6] = self.shm.net_denied }) + [self.icmp_in:code()] + or self.shm.code_not_implemented_errors + ) +end + +-- Packet Too Big +ICMP6.handlers[2] = function (self, p) + self:log(p) + packet.free(p) + counter.add(self.shm.packet_too_big) +end + +-- Time Exceeded +ICMP6.handlers[3] = function (self, p) + self:log(p) + packet.free(p) + counter.add(self.shm.time_exceeded) + counter.add( + ({ [0] = self.shm.transit_hop_limit_exceeded, + [1] = self.shm.fragment_reassembly_time_exceeded }) + [self.icmp_in:code()] + or self.shm.code_not_implemented_errors + ) +end + +-- Parameter Problem +ICMP6.handlers[4] = function (self, p) + self:log(p) + packet.free(p) + counter.add(self.shm.parameter_problem) + counter.add( + ({ [0] = self.shm.header_field_problem, + [1] = self.shm.next_header_problem, + [2] = self.shm.option_problem }) + [self.icmp_in:code()] + or self.shm.code_not_implemented_errors + ) +end + +-- Echo +ICMP6.handlers[128] = function (self, p) + -- Copy payload. + local reply = packet.from_pointer(self:msg_payload(p)) + -- Prepend ICMP and IP headers. + self.ip6:dst(self.ip6_in:src()) + self.ip6:payload_length(reply.length + icmp:sizeof()) + self.icmp:type(129) + self.icmp:code(0) + self.icmp:checksum(reply.data, reply.length, self.ip6) + reply = packet.prepend(reply, self.icmp:header(), icmp:sizeof()) + reply = packet.prepend(reply, self.ip6:header(), ipv6:sizeof()) + -- Send reply. + link.transmit(self.output.output, reply) + packet.free(p) + counter.add(self.shm.echo_request) +end + +function ICMP6:msg_payload (p) + local payload = p.data + ICMP6.payload_offset + local declared_length = self.ip6_in:payload_length() - icmp:sizeof() + local actual_length = p.length - ICMP6.payload_offset + local length = max(0, min(actual_length, declared_length)) + return payload, length +end + +function ICMP6:handle_msg (p) + -- Ensure packet is a valid ICMP message. + local function parse_and_check (p) + if not self.ip6_in:new_from_mem(p.data, p.length) + or self.ip6_in:next_header() ~= ICMP6.PROTOCOL + or not self.icmp_in:new_from_mem(p.data + ipv6:sizeof(), + p.length - ipv6:sizeof()) + then return false end + local payload, length = self:msg_payload(p) + return self.icmp_in:checksum_check(payload, length, self.ip6_in) + end + if not parse_and_check(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_in: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 ICMP6:send_msg (msgtype, code, p, opt) + opt = opt or {} + local msg = packet.resize(packet.allocate(), ffi.sizeof(ICMP6.payload_t)) + local payload = ffi.cast(ICMP6.payload_ptr_t, msg.data) + -- Set fields, copy packet excerpt. + if opt.nexthop_mtu then + payload.fragmentation_needed.nexthop_mtu = lib.htonl(opt.nexthop_mtu) + end + if opt.pointer then + payload.parameter_problem.pointer = lib.htonl(opt.pointer) + end + local excerpt_max = -- As much of invoking packet as possible without the + -- ICMPv6 packet exceeding the minimum IPv6 MTU + ICMP6.MIN_MTU - ICMP6.payload_offset - ffi.sizeof(payload.base.unused) + local excerpt_length = min(p.length, excerpt_max) + msg = packet.resize(msg, ffi.sizeof(payload.base.unused) + excerpt_length) + ffi.copy(payload.base.excerpt, p.data, excerpt_length) + -- Prepend ICMP and IP headers + assert(self.ip6_in:new_from_mem(p.data, p.length)) + self.ip6:dst(self.ip6_in:src()) + self.ip6:payload_length(msg.length + icmp:sizeof()) + self.icmp:type(msgtype) + self.icmp:code(code) + self.icmp:checksum(msg.data, msg.length, self.ip6) + msg = packet.prepend(msg, self.icmp:header(), icmp:sizeof()) + msg = packet.prepend(msg, self.ip6:header(), ipv6:sizeof()) + -- Send message, free packet. + link.transmit(self.output.output, msg) + packet.free(p) +end + +function ICMP6: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 ICMP6: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(4, 1, link.receive(self.input.protocol_unreachable), { + pointer = 6 -- Next Header + }) + end + end + if self.input.packet_too_big then + while not link.empty(self.input.packet_too_big) + and self:rate_limit('tx') do + self:send_msg(2, 0, link.receive(self.input.packet_too_big), { + nexthop_mtu = self.nexthop_mtu + }) + end + end + if self.input.transit_hop_limit_exceeded then + while not link.empty(self.input.transit_hop_limit_exceeded) + and self:rate_limit('tx') do + self:send_msg(3, 0, link.receive(self.input.transit_hop_limit_exceeded)) + end + end + -- ...remainder is NYI. +end diff --git a/src/program/vita/schemata.lua b/src/program/vita/schemata.lua index 541f43d818..431ff542d1 100644 --- a/src/program/vita/schemata.lua +++ b/src/program/vita/schemata.lua @@ -75,6 +75,7 @@ function compute_state_reader (schema_name) PublicRouter = "public-router", PrivateICMP4 = "private-icmp4", PublicICMP4 = "public-icmp4", + PublicICMP6 = "public-icmp6", InboundICMP4 = "inbound-icmp4", KeyManager = "key-manager" } diff --git a/src/program/vita/selftest.snabb b/src/program/vita/selftest.snabb index f0c62f7881..7502befcd9 100755 --- a/src/program/vita/selftest.snabb +++ b/src/program/vita/selftest.snabb @@ -28,20 +28,20 @@ local ffi = require("ffi") local regenerate_pcaps = main.parameters[1] local cfg = { - private_interface = { + private_interface4 = { pci = "00:00.0", ip4 = "192.168.10.1", nexthop_ip4 = "192.168.0.1", mac = "52:54:00:00:00:00" }, - public_interface = { + public_interface4 = { pci = "00:00.0", ip4 = "203.0.113.1", nexthop_ip4 = "203.0.0.1", mac = "52:54:00:00:00:FF" }, mtu = 500, - route = { + route4 = { loopback = { net_cidr4 = "192.168.10.0/24", gw_ip4 = "203.0.113.1", diff --git a/src/program/vita/selftest6-pcaps.snabb b/src/program/vita/selftest6-pcaps.snabb new file mode 100755 index 0000000000..1f528ff4a6 --- /dev/null +++ b/src/program/vita/selftest6-pcaps.snabb @@ -0,0 +1,256 @@ +#!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 ipv6 = require("lib.protocol.ipv6") +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 selftest6-pcaps.snabb +-- +-- Source selftest-*-in.pcap with packets that exercise various corner cases in +-- Vita. Anything that’s not the happy path. IPv6 version. + +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 public = PcapLog:new("program/vita/selftest6-public-in.pcap") + +local private_src = ipv4:pton("192.168.0.1") +local private_dst = ipv4:pton("192.168.10.1") +local public_src = ipv6:pton("203:0:0::1") +local public_dst = ipv6: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 + +function icmp6 (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) + local ip6 = ipv6:new{ + payload_length = icmp:sizeof() + length, + hop_limit = conf.ttl or 64, + next_header = conf.protocol or 58, + src = conf.src, + dst = conf.dst + } + icm:checksum(msg:payload(), conf.payload_length or payload.length, ip6) + icm:header().checksum = conf.icmp_checksum or icm:header().checksum + msg:push(icm) + msg:push(ip6) + msg:push(ethernet:new{type=0x86dd}) + 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 encap6 (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(ipv6:new{ + payload_length = (conf.length or payload.length), + hop_limit = conf.ttl or 64, + next_header = esp.PROTOCOL, + src = conf.src, + dst = conf.dst + }) + d:push(ethernet:new{type=0x86dd}) + return d:packet() +end + +-- Echo request +public:write(icmp6{ + type = 128, + src = public_src, + dst = public_dst +}) +-- Broken echo request (too short) +public:write(icmp6{ + type = 128, + src = public_src, + dst = public_dst, + payload_length = 10000 +}) +-- Broken echo request (too long) +public:write(icmp6{ + type = 128, + src = public_src, + dst = public_dst, + payload_length = 4 +}) +-- Echo reply +public:write(icmp6{ + type = 129, + src = public_src, + dst = public_dst +}) +-- Encapsulated echo request +public:write(encap6( + icmp4{ + type = 8, + src = remote_dst, + dst = private_dst + }, + { + src = public_src, + dst = public_dst + } +)) +-- Unreachable protocol (private/public/inbound) +public:write(icmp6{ + protocol = 42, + src = public_src, + dst = public_dst, + payload = packet.from_string(("x"):rep(2000)) +}) +public:write(encap6( + icmp4{ + protocol = 42, + src = remote_dst, + dst = private_dst + }, + { + src = public_src, + dst = public_dst + } +)) +-- TTL expired (private/inbound) +public:write(encap6( + icmp4{ + type = 8, + src = remote_dst, + dst = remote_dst, + ttl = 0 + }, + { + src = public_src, + dst = public_dst + } +)) +-- Bogus SPI +public:write(encap6( + icmp4{ + type = 8, + src = remote_dst, + dst = private_dst + }, + { + sa = sa_bad_spi, + src = public_src, + dst = public_dst + } +)) +-- Bogus SeqNo +public:write(encap6( + icmp4{ + type = 8, + src = remote_dst, + dst = private_dst + }, + { + sa = sa_replay, + src = public_src, + dst = public_dst + } +)) +-- Bogus NextHeader +public:write(encap6( + icmp4{ + type = 8, + src = remote_dst, + dst = private_dst + }, + { + src = public_src, + dst = public_dst, + nh = 42 + } +)) +-- Bogus checksums +public:write(icmp6{ + type = 128, + src = public_src, + dst = public_dst, + icmp_checksum = 42 +}) +-- Various ICMPv6 messages +local payload = packet.from_string("....012345678901234567890123456789012345") +for _, msgtype in ipairs({ + {type=1, codes={0,1,2,3,4,5,6,100}}, + {type=2, codes={0,100}}, + {type=3, codes={0,1,100}}, + {type=4, codes={0,1,2,100}}, + {type=100, codes={0}} +}) do + for _, code in ipairs(msgtype.codes) do + public:write(icmp6{ + payload = packet.clone(payload), + type = msgtype.type, + code = code, + src = public_src, + dst = public_dst + }) + end +end diff --git a/src/program/vita/selftest6-private-out.pcap b/src/program/vita/selftest6-private-out.pcap new file mode 100644 index 0000000000..a324304509 Binary files /dev/null and b/src/program/vita/selftest6-private-out.pcap differ diff --git a/src/program/vita/selftest6-public-in.pcap b/src/program/vita/selftest6-public-in.pcap new file mode 100644 index 0000000000..cd1ed16481 Binary files /dev/null and b/src/program/vita/selftest6-public-in.pcap differ diff --git a/src/program/vita/selftest6-public-out.pcap b/src/program/vita/selftest6-public-out.pcap new file mode 100644 index 0000000000..9b26ab1773 Binary files /dev/null and b/src/program/vita/selftest6-public-out.pcap differ diff --git a/src/program/vita/selftest6.snabb b/src/program/vita/selftest6.snabb new file mode 100755 index 0000000000..d5bb414492 --- /dev/null +++ b/src/program/vita/selftest6.snabb @@ -0,0 +1,209 @@ +#!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 ipv6 = require("lib.protocol.ipv6") +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") +local cltable = require("lib.cltable") +local ffi = require("ffi") + +-- Synopsis: +-- +-- sudo selftest6.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. IPv6 version. +-- +-- TODO: doesn’t exercise KeyManager yet. + +local regenerate_pcaps = main.parameters[1] + +local cfg = { + private_interface4 = { + pci = "00:00.0", + ip4 = "192.168.10.1", + nexthop_ip4 = "192.168.0.1", + mac = "52:54:00:00:00:00" + }, + public_interface6 = { + pci = "00:00.0", + ip6 = "203:0:113::1", + nexthop_ip6 = "203:0:0::1", + mac = "52:54:00:00:00:FF", + nexthop_mac = "52:54:00:00:00:FE" + }, + mtu = 500, + route46 = { + loopback = { + net_cidr4 = "192.168.10.0/24", + gw_ip6 = "203:0:113::1", + preshared_key = string.rep("00", 32), + spi = 1001 + } + }, + outbound_sa = { + [1001] = { + route = "loopback", + aead = "aes-gcm-16-icv", + key = "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00", + salt = "00 00 00 00" + } + }, + inbound_sa = { + [1001] = { + route = "loopback", + aead = "aes-gcm-16-icv", + key = "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00", + salt = "00 00 00 00", + auditing = true + } + } +} + +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(cfg, c) +vita.configure_dsp(cfg, 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.link(c, "private_arp.south -> "..private.input) +config.link(c, private.output.." -> private_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.output.." -> public_out.input") +config.link(c, "public_out.loopback -> filter.input") +config.link(c, "filter.output -> join.loopback") +config.link(c, "join.output -> "..public.input) + +-- Add PCAP sources and sinks. +--config.app(c, "private_pcap_in", pcap.PcapReader, +-- "program/vita/selftest-private-in.pcap") +config.app(c, "private_pcap_in", basic_apps.Sink) -- null +config.app(c, "public_pcap_in", pcap.PcapReader, + "program/vita/selftest6-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/selftest6-private-out.pcap") + config.app(c, "public_pcap_out", pcap.PcapWriter, + "program/vita/selftest6-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/selftest6-private-out.pcap") + config.app(c, "public_pcap_out", pcap.PcapReader, + "program/vita/selftest6-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.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{ + PublicRouter = { + route_errors = 1, -- Bogus SPI + }, + DSP_loopback_1001 = { -- Bogus SeqNo, Bogus NextHeader + rxerrors = 2, + protocol_errors = 1, + decrypt_errors = 1 + }, + PublicICMP6 = { + echo_request = 2, -- Echo request, Broken echo request (too long) + destination_unreachable = 8,-- Standard codes, Code 100 + net_unreachable = 1, + destination_denied = 1, + scope_denied = 1, + host_unreachable = 1, + port_unreachable = 1, + source_denied = 1, + net_denied = 1, + packet_too_big = 2, -- Code 0, Code 100 + time_exceeded = 3, -- Standard codes, Code 100 + fragment_reassembly_time_exceeded = 1, + transit_hop_limit_exceeded = 1, + parameter_problem = 4, -- Standard codes, Code 100 + next_header_problem = 1, + header_field_problem = 1, + option_problem = 1, + rxerrors = 4, + protocol_errors = 2, -- Broken echo request (too short), Bogus checksum + type_not_implemented_errors = 2, -- Echo reply, Type 100 + code_not_implemented_errors = 3 -- Code 100 + }, + InboundICMP4 = { -- Encapsulated echo request + 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.lua b/src/program/vita/test.lua index 7bffdef583..a8e6d34b72 100644 --- a/src/program/vita/test.lua +++ b/src/program/vita/test.lua @@ -107,21 +107,28 @@ end -- Testing setups for Vita -- Run Vita in software benchmark mode. -function run_softbench (pktsize, npackets, nroutes, cpuspec) +function run_softbench (pktsize, npackets, nroutes, cpuspec, use_v6) local testconf = { - private_interface = { - nexthop_ip4 = private_interface_defaults.ip4.default + private_interface4 = { + nexthop_ip4 = private_interface4_defaults.ip4.default }, packet_size = pktsize, nroutes = nroutes, negotiation_ttl = nroutes, sa_ttl = 16 } + if use_v6 then + testconf.public_interface6 = { + ip6 = public_interface6_defaults.ip6.default + } + end local function configure_private_router_softbench (conf) local c, private = vita.configure_private_router(conf) - if not conf.private_interface then return c end + if not (conf.private_interface4 or conf.private_interface6) then + return c + end config.app(c, "bridge", basic_apps.Join) config.link(c, "bridge.output -> "..private.input) @@ -175,7 +182,9 @@ end function configure_public_router_loopback (conf, append) local c, public = vita.configure_public_router(conf, append) - if not conf.public_interface then return c end + if not (conf.public_interface4 or conf.public_interface6) then + return c + end config.link(c, public.output.." -> "..public.input) @@ -191,28 +200,36 @@ end -- destination. defaults = { - private_interface = {}, - public_interface = {}, + private_interface4 = {}, + public_interface4 = {}, + public_interface6 = {}, route_prefix = {default="172.16"}, nroutes = {default=1}, packet_size = {default="IMIX"}, sa_ttl = {}, negotiation_ttl = {default=1} } -private_interface_defaults = { +private_interface4_defaults = { pci = {default="00:00.0"}, mac = {default="02:00:00:00:00:01"}, -- needed because used in sim. packets ip4 = {default="172.16.0.10"}, nexthop_ip4 = {default="172.16.1.1"}, nexthop_mac = {} } -public_interface_defaults = { +public_interface4_defaults = { pci = {default="00:00.0"}, mac = {}, ip4 = {default="172.16.0.10"}, nexthop_ip4 = {default="172.16.0.10"}, nexthop_mac = {} } +public_interface6_defaults = { + pci = {default="00:00.0"}, + mac = {}, + ip6 = {default="172:16:0::10"}, + nexthop_ip6 = {default="172:16:0::10"}, + nexthop_mac = {} +} traffic_templates = { -- Internet Mix, see https://en.wikipedia.org/wiki/Internet_Mix @@ -220,11 +237,19 @@ traffic_templates = { } local function parse_gentestconf (conf) + -- default to v4 + conf.private_interface4 = (not conf.private_interface6) and + (conf.private_interface4 or {}) + conf.public_interface4 = (not conf.public_interface6) and + (conf.public_interface4 or {}) + -- populate defaults conf = lib.parse(conf, defaults) - conf.private_interface = lib.parse(conf.private_interface, - private_interface_defaults) - conf.public_interface = lib.parse(conf.public_interface, - public_interface_defaults) + conf.private_interface4 = conf.private_interface4 and + lib.parse(conf.private_interface4, private_interface4_defaults) + conf.public_interface4 = conf.public_interface4 and + lib.parse(conf.public_interface4, public_interface4_defaults) + conf.public_interface6 = conf.public_interface6 and + lib.parse(conf.public_interface6, public_interface6_defaults) assert(conf.nroutes >= 0 and conf.nroutes <= 255, "Invalid number of routes: "..conf.nroutes) return conf @@ -234,11 +259,11 @@ function gen_packet (conf, route, size) local payload_size = size - ethernet:sizeof() - ipv4:sizeof() assert(payload_size >= 0, "Negative payload_size :-(") local d = datagram:new(packet.resize(packet.allocate(), payload_size)) - d:push(ipv4:new{ src = ipv4:pton(conf.private_interface.nexthop_ip4), + d:push(ipv4:new{ src = ipv4:pton(conf.private_interface4.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_interface.mac), + d:push(ethernet:new{ dst = ethernet:pton(conf.private_interface4.mac), type = 0x0800 }) local p = d:packet() -- Pad to minimum Ethernet frame size (excluding four octet CRC) @@ -263,16 +288,22 @@ end function gen_configuration (conf) conf = parse_gentestconf(conf) local cfg = { - private_interface = conf.private_interface, - public_interface = conf.public_interface, - route = {}, + private_interface4 = conf.private_interface4, + public_interface4 = conf.public_interface4, + public_interface6 = conf.public_interface6, + route4 = {}, + route46 = {}, negotiation_ttl = conf.negotiation_ttl, sa_ttl = conf.sa_ttl } + local routes = + (cfg.private_interface4 and cfg.public_interface4 and cfg.route4) or + (cfg.private_interface4 and cfg.public_interface6 and cfg.route46) for route = 1, conf.nroutes do - cfg.route["test"..route] = { + routes["test"..route] = { net_cidr4 = conf.route_prefix.."."..route..".0/24", - gw_ip4 = conf.public_interface.nexthop_ip4, + gw_ip4 = cfg.public_interface4 and cfg.public_interface4.nexthop_ip4, + gw_ip6 = cfg.public_interface6 and cfg.public_interface6.nexthop_ip6, preshared_key = ("%064x"):format(route), spi = 1000+route } diff --git a/src/program/vita/test6.snabb b/src/program/vita/test6.snabb new file mode 100755 index 0000000000..d35dd3143e --- /dev/null +++ b/src/program/vita/test6.snabb @@ -0,0 +1,14 @@ +#!snabb snsh + +-- Use of this source code is governed by the GNU AGPL license; see COPYING. + +local vita_test = require("program.vita.test") + +-- IPv6 version of test.snabb + +local pktsize = tonumber(main.parameters[1]) or main.parameters[1] +local npackets = tonumber(main.parameters[2]) or 10e6 +local nroutes = tonumber(main.parameters[3]) +local cpuspec = main.parameters[4] + +vita_test.run_softbench(pktsize, npackets, nroutes, cpuspec, 'IPv6') diff --git a/src/program/vita/tunnel.lua b/src/program/vita/tunnel.lua index e317d3713b..3e0138c9ab 100644 --- a/src/program/vita/tunnel.lua +++ b/src/program/vita/tunnel.lua @@ -5,6 +5,7 @@ module(...,package.seeall) local counter = require("core.counter") local esp = require("lib.ipsec.esp") local ipv4 = require("lib.protocol.ipv4") +local ipv6 = require("lib.protocol.ipv6") -- sa := { spi=(SPI), aead=(STRING), key=(KEY), salt=(SALT), -- [ window_size=(INT), @@ -13,6 +14,7 @@ local ipv4 = require("lib.protocol.ipv4") -- https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml local NextHeaderIPv4 = 4 +local NextHeaderIPv6 = 41 Encapsulate = { name = "Encapsulate", @@ -30,12 +32,22 @@ end function Encapsulate:push () local output, sa = self.output.output, self.sa - local input4 = self.input.input4 - while not link.empty(input4) do - link.transmit( - output, - sa:encapsulate_tunnel(link.receive(input4), NextHeaderIPv4) - ) + local input4, input6 = self.input.input4, self.input.input6 + if input4 then + while not link.empty(input4) do + link.transmit( + output, + sa:encapsulate_tunnel(link.receive(input4), NextHeaderIPv4) + ) + end + end + if input6 then + while not link.empty(input6) do + link.transmit( + output, + sa:encapsulate_tunnel(link.receive(input6), NextHeaderIPv6) + ) + end end end @@ -65,12 +77,14 @@ end function Decapsulate:push () local input, sa = self.input.input, self.sa - local output4 = self.output.output4 + local output4, output6 = self.output.output4, self.output.output6 while not link.empty(input) do local p_enc = link.receive(input) local p, next_header = sa:decapsulate_tunnel(p_enc) - if p and next_header == NextHeaderIPv4 then + if p and next_header == NextHeaderIPv4 and output4 then link.transmit(output4, p) + elseif p and next_header == NextHeaderIPv6 and output6 then + link.transmit(output6, p) elseif p then counter.add(self.shm.rxerrors) counter.add(self.shm.protocol_errors) @@ -120,3 +134,37 @@ function Tunnel4:encapsulate (p) self.ip:checksum() return p end + + +Tunnel6 = { + name = "Tunnel6", + config = { + src = {required=true}, + dst = {required=true} + } +} + +function Tunnel6:new (conf) + local o = { + ip_template = ipv6:new{ + src = ipv6:pton(conf.src), + dst = ipv6:pton(conf.dst), + next_header = esp.PROTOCOL, + hop_limit = 64 + } + } + return setmetatable(o, {__index = Tunnel6}) +end + +function Tunnel6:push () + local input, output = self.input.input, self.output.output + while not link.empty(input) do + link.transmit(output, self:encapsulate(link.receive(input))) + end +end + +function Tunnel6:encapsulate (p) + self.ip_template:payload_length(p.length) + p = packet.prepend(p, self.ip_template:header(), ipv6:sizeof()) + return p +end diff --git a/src/program/vita/vita-esp-gateway.yang b/src/program/vita/vita-esp-gateway.yang index 64b2f07bfa..11a8af32f9 100644 --- a/src/program/vita/vita-esp-gateway.yang +++ b/src/program/vita/vita-esp-gateway.yang @@ -8,28 +8,50 @@ module vita-esp-gateway { import ietf-inet-types { prefix inet; } // APPLICATION MODEL - grouping interface { + grouping interface4 { leaf pci { type pci-address; mandatory true; } - leaf ip4 { type inet:ipv4-address-no-zone; mandatory true; } + leaf ip4 { type inet:ipv4-address-no-zone; } leaf mac { type yang:mac-address; } - leaf nexthop-ip4 { type inet:ipv4-address-no-zone; mandatory true; } + leaf nexthop-ip4 { type inet:ipv4-address-no-zone; } + leaf nexthop-mac { type yang:mac-address; } + } + grouping interface6 { + leaf pci { type pci-address; mandatory true; } + leaf ip6 { type inet:ipv6-address-no-zone; } + leaf mac { type yang:mac-address; } + leaf nexthop-ip6 { type inet:ipv6-address-no-zone; } leaf nexthop-mac { type yang:mac-address; } } - container private-interface { uses interface; } - container public-interface { uses interface; } - - leaf mtu { type uint16 { range "0..8937"; } } + grouping route { + leaf id { type id; mandatory true; } + leaf spi { type spi; mandatory true; } + leaf preshared-key { type key32; mandatory true; } + } - list route { - key id; unique "net-cidr4"; unique "preshared-key"; unique "spi"; - leaf id { type id; mandatory true; } - leaf net-cidr4 { type inet:ipv4-prefix; mandatory true; } - leaf gw-ip4 { type inet:ipv4-address-no-zone; mandatory true; } - leaf spi { type spi; mandatory true; } - leaf preshared-key { type key32; mandatory true; } + choice router { + case v4-over-v4 { + container private-interface4 { uses interface4; } + container public-interface4 { uses interface4; } + list route4 { + key id; unique "net-cidr4"; unique "preshared-key"; unique "spi"; uses route; + leaf net-cidr4 { type inet:ipv4-prefix; mandatory true; } + leaf gw-ip4 { type inet:ipv4-address-no-zone; } + } + } + case v4-over-v6 { + container private-interface4 { uses interface4; } + container public-interface6 { uses interface6; } + list route46 { + key id; unique "net-cidr4"; unique "preshared-key"; unique "spi"; uses route; + leaf net-cidr4 { type inet:ipv4-prefix; mandatory true; } + leaf gw-ip6 { type inet:ipv6-address-no-zone; } + } + } } + leaf mtu { type uint16 { range "0..8937"; } } + leaf negotiation-ttl { type time-to-live; } leaf sa-ttl { type time-to-live; } @@ -411,6 +433,131 @@ module vita-esp-gateway { } } + grouping icmp6-state { + leaf rxerrors { + type yang:zero-based-counter64; + description + "Total ICMPv6 messages that were dropped because they were invalid."; + } + leaf protocol-errors { + type yang:zero-based-counter64; + description + "Count of ICMPv6 messages that were dropped because their checksum + was invalid, or they were IP fragments."; + } + leaf type-not-implemented-errors { + type yang:zero-based-counter64; + description + "Count of ICMPv6 messages received that had an unrecognized type."; + } + leaf code-not-implemented-errors { + type yang:zero-based-counter64; + description + "Count of ICMPv6 messages received that had an unrecognized code."; + } + leaf destination-unreachable { + type yang:zero-based-counter64; + description + "Count of ICMPv6 messages received of type + 'destination unreachable'."; + } + leaf net-unreachable { + type yang:zero-based-counter64; + description + "Count of ICMPv6 'destination unreachable' messages received with + code 'no route to destination'."; + } + leaf destination-denied { + type yang:zero-based-counter64; + description + "Count of ICMPv6 'destination unreachable' messages received with + code 'communication with destination administratively prohibited'."; + } + leaf scope-denied { + type yang:zero-based-counter64; + description + "Count of ICMPv6 'destination unreachable' messages received with + code 'beyond scope of source address'."; + } + leaf host-unreachable { + type yang:zero-based-counter64; + description + "Count of ICMPv6 'destination unreachable' messages received with + code 'address unreachable'."; + } + leaf port-unreachable { + type yang:zero-based-counter64; + description + "Count of ICMPv6 'destination unreachable' messages received with + code 'port unreachable'."; + } + leaf source-denied { + type yang:zero-based-counter64; + description + "Count of ICMPv6 'destination unreachable' messages received with + code 'source address failed ingress/egress policy'."; + } + leaf net-denied { + type yang:zero-based-counter64; + description + "Count of ICMPv6 'destination unreachable' messages received with + code 'reject route to destination'."; + } + leaf packet-too-big { + type yang:zero-based-counter64; + description + "Count of ICMPv6 messages received of type + 'packet too big'."; + } + leaf time-exceeded { + type yang:zero-based-counter64; + description + "Count of ICMPv6 messages received of type + 'time exceeded'."; + } + leaf transit-hop-limit-exceeded { + type yang:zero-based-counter64; + description + "Count of ICMPv6 'time exceeded' messages received with code + 'hop limit exceeded in transit'."; + } + leaf fragment-reassembly-time-exceeded { + type yang:zero-based-counter64; + description + "Count of ICMPv6 'time exceeded' messages received with code + 'fragment reassembly time exceeded'."; + } + leaf parameter-problem { + type yang:zero-based-counter64; + description + "Count of ICMPv6 messages received of type + 'parameter problem'."; + } + leaf header-field-problem { + type yang:zero-based-counter64; + description + "Count of ICMPv6 'parameter exceeded' messages received with code + 'erroneous header field encountered'."; + } + leaf next-header-problem { + type yang:zero-based-counter64; + description + "Count of ICMPv6 'parameter exceeded' messages received with code + 'unrecognized next header type encountered'."; + } + leaf option-problem { + type yang:zero-based-counter64; + description + "Count of ICMPv6 'parameter exceeded' messages received with code + 'unrecognized IPv6 option encountered'."; + } + leaf echo-request { + type yang:zero-based-counter64; + description + "Count of ICMPv6 echo requests handled."; + } + } + container private-icmp4 { description "ICMPv4 events on the private interface."; uses icmp4-state; @@ -421,6 +568,11 @@ module vita-esp-gateway { uses icmp4-state; } + container public-icmp6 { + description "ICMPv6 events on the public interface."; + uses icmp6-state; + } + container inbound-icmp4 { description "ICMPv4 events triggered by encapsulated messages on the public interface."; diff --git a/src/program/vita/vita-gentest.yang b/src/program/vita/vita-gentest.yang index 249705b10f..df276884b8 100644 --- a/src/program/vita/vita-gentest.yang +++ b/src/program/vita/vita-gentest.yang @@ -4,8 +4,16 @@ module vita-gentest { import vita-esp-gateway { prefix vita; } - container private-interface { uses vita:interface; } - container public-interface { uses vita:interface; } + choice router { + case v4-over-v4 { + container private-interface4 { uses vita:interface4; } + container public-interface4 { uses vita:interface4; } + } + case v4-over-v6 { + container private-interface4 { uses vita:interface4; } + container public-interface6 { uses vita:interface6; } + } + } leaf negotiation-ttl { type vita:time-to-live; } leaf sa-ttl { type vita:time-to-live; } diff --git a/src/program/vita/vita-loadtest.md b/src/program/vita/vita-loadtest.md index d7af6bc8af..aa63fa2430 100644 --- a/src/program/vita/vita-loadtest.md +++ b/src/program/vita/vita-loadtest.md @@ -74,18 +74,18 @@ First of all, we start two Vita nodes, and assign them the names `node1` and `no Most of it should be fairly self explanatory: we assign the desired ports via their PCI bus addresses, and set the interface’s own addresses as well as the addresses of the next hops. In this case, the private and public interface addresses are the same, but they need not be. The next hop of the private interface (this would normally be your local router) will be `snabb loadtest`. Since `loadtest` does not speak ARP, we configure a fixed MAC destination address for this next hop. This will prevent Vita from attempting to look up the next hop’s MAC addresses via ARP, and instead use the preconfigured address. The next hop of the public interface (this would normally be your gateway to the Internet) is configured to be the other Vita node in the test setup. Finally, we define a single route to the subnet `172.17.1.0/24` via the second Vita node with a dummy key. For the other Vita node, `node2.conf` is symmetric: - private-interface { + private-interface4 { pci 22:00.2; ip4 172.17.0.10; nexthop-ip4 172.17.0.1; nexthop-mac 02:00:00:00:00:00; } - public-interface { + public-interface4 { pci 23:00.1; ip4 172.17.0.10; nexthop-ip4 172.16.0.10; } - route { + route4 { id test1; gw-ip4 172.16.0.10; net-cidr4 "172.16.1.0/24"; @@ -95,13 +95,13 @@ Most of it should be fairly self explanatory: we assign the desired ports via th Because typing out configuration files for testing gets old fast, and we still need matching Pcap records, Vita comes with a utility that generates both of these from a meta-configuration file. For the first node we have `gentest-node1.conf`: - private-interface { + private-interface4 { pci 23:00.0; ip4 172.16.0.10; nexthop-ip4 172.16.0.1; nexthop-mac 02:00:00:00:00:00; } - public-interface { + public-interface4 { pci 22:00.1; ip4 172.16.0.10; nexthop-ip4 172.17.0.10; @@ -112,13 +112,13 @@ Because typing out configuration files for testing gets old fast, and we still n …and for the second node `gentest-node2.conf`: - private-interface { + private-interface4 { pci 22:00.2; ip4 172.17.0.10; nexthop-ip4 172.17.0.1; nexthop-mac 02:00:00:00:00:00; } - public-interface { + public-interface4 { pci 23:00.1; ip4 172.17.0.10; nexthop-ip4 172.16.0.10; diff --git a/src/program/vita/vita.lua b/src/program/vita/vita.lua index e92732665c..8100573bda 100644 --- a/src/program/vita/vita.lua +++ b/src/program/vita/vita.lua @@ -16,8 +16,11 @@ local icmp = require("program.vita.icmp") local Receiver = require("apps.interlink.receiver") local Transmitter = require("apps.interlink.transmitter") local intel_mp = require("apps.intel_mp.intel_mp") +local nd_light = require("apps.ipv6.nd_light").nd_light +local Join = require("apps.basic.basic_apps").Join local ethernet = require("lib.protocol.ethernet") local ipv4 = require("lib.protocol.ipv4") +local ipv6 = require("lib.protocol.ipv6") local numa = require("lib.numa") local yang = require("lib.yang.yang") local ptree = require("lib.ptree.ptree") @@ -29,10 +32,12 @@ local usage = require("program.vita.README_inc") local confighelp = require("program.vita.README_config_inc") local confspec = { - private_interface = {}, - public_interface = {}, + private_interface4 = {}, + public_interface4 = {}, + public_interface6 = {}, mtu = {default=8923}, - route = {default={}}, + route4 = {default={}}, + route46 = {default={}}, negotiation_ttl = {}, sa_ttl = {}, data_plane = {}, @@ -42,17 +47,21 @@ local confspec = { local ifspec = { pci = {required=true}, - ip4 = {required=true}, - nexthop_ip4 = {required=true}, + ip4 = {}, + ip6 = {}, + nexthop_ip4 = {}, + nexthop_ip6 = {}, mac = {}, nexthop_mac = {} } -local function derive_local_unicast_mac (prefix, ip4) +local function derive_local_unicast_mac (prefix, ip) local mac = ffi.new("uint8_t[?]", 6) mac[0] = prefix[1] mac[1] = prefix[2] - ffi.copy(mac+2, ipv4:pton(ip4), 4) + local n, offset = ipv4:pton(ip), 0 + if not n then n, offset = ipv6:pton(ip), 12 end + ffi.copy(mac+2, ffi.cast("uint8_t *", n) + offset, 4) -- First bit = 0 indicates unicast, second bit = 1 means locally -- administered. assert(bit.band(bit.bor(prefix[1], 0x02), 0xFE) == prefix[1], @@ -63,14 +72,16 @@ end local function parse_ifconf (conf, mac_prefix) if not conf then return end conf = lib.parse(conf, ifspec) - conf.mac = conf.mac or derive_local_unicast_mac(mac_prefix, conf.ip4) + conf.mac = conf.mac or + derive_local_unicast_mac(mac_prefix, conf.ip4 or conf.ip6) return conf end local function parse_conf (conf) conf = lib.parse(conf, confspec) - conf.private_interface = parse_ifconf(conf.private_interface, {0x2a, 0xbb}) - conf.public_interface = parse_ifconf(conf.public_interface, {0x3a, 0xbb}) + conf.private_interface4 = parse_ifconf(conf.private_interface4, {0x2a,0xbb}) + conf.public_interface4 = parse_ifconf(conf.public_interface4, {0x3a,0xbb}) + conf.public_interface6 = parse_ifconf(conf.public_interface6, {0x3a,0xbb}) return conf end @@ -214,32 +225,38 @@ function configure_private_router (conf, append) conf = parse_conf(conf) local c = append or config.new() - if not conf.private_interface then return c end + local interface = conf.private_interface4 + if not interface then return c end + + local routes = (conf.public_interface4 and conf.route4) or + (conf.public_interface6 and conf.route46) + + local private_links = {} config.app(c, "PrivateDispatch", dispatch.PrivateDispatch, { - node_ip4 = conf.private_interface.ip4 + node_ip4 = interface.ip4 }) config.app(c, "OutboundTTL", ttl.DecrementTTL) config.app(c, "PrivateRouter", route.PrivateRouter, { - routes = conf.route, + routes = routes, mtu = conf.mtu }) config.app(c, "PrivateICMP4", icmp.ICMP4, { - node_ip4 = conf.private_interface.ip4, + node_ip4 = interface.ip4, nexthop_mtu = conf.mtu }) config.app(c, "InboundDispatch", dispatch.InboundDispatch, { - node_ip4 = conf.private_interface.ip4 + node_ip4 = interface.ip4 }) config.app(c, "InboundTTL", ttl.DecrementTTL) config.app(c, "InboundICMP4", icmp.ICMP4, { - node_ip4 = conf.private_interface.ip4 + node_ip4 = interface.ip4 }) config.app(c, "PrivateNextHop", nexthop.NextHop4, { - node_mac = conf.private_interface.mac, - node_ip4 = conf.private_interface.ip4, - nexthop_ip4 = conf.private_interface.nexthop_ip4, - nexthop_mac = conf.private_interface.nexthop_mac + node_mac = interface.mac, + node_ip4 = interface.ip4, + nexthop_ip4 = interface.nexthop_ip4, + nexthop_mac = interface.nexthop_mac }) config.link(c, "PrivateDispatch.forward4 -> OutboundTTL.input") config.link(c, "PrivateDispatch.icmp4 -> PrivateICMP4.input") @@ -255,8 +272,10 @@ function configure_private_router (conf, append) config.link(c, "InboundTTL.output -> PrivateNextHop.forward") config.link(c, "InboundTTL.time_exceeded -> InboundICMP4.transit_ttl_exceeded") config.link(c, "InboundICMP4.output -> PrivateRouter.control") + private_links.input = "PrivateDispatch.input" + private_links.output = "PrivateNextHop.output" - for id, route in pairs(conf.route) do + for id, route in pairs(routes) do local private_in = "PrivateRouter."..id local ESP_in = "ESP_"..id.."_in" config.app(c, ESP_in.."_Tx", Transmitter, ESP_in) @@ -270,10 +289,6 @@ function configure_private_router (conf, append) config.link(c, DSP_out.."_Rx.output -> "..private_out) end - local private_links = { - input = "PrivateDispatch.input", - output = "PrivateNextHop.output" - } return c, private_links end @@ -281,28 +296,62 @@ function configure_public_router (conf, append) conf = parse_conf(conf) local c = append or config.new() - if not conf.public_interface then return c end + local interface = conf.public_interface4 or conf.public_interface6 + if not interface then return c end + + local routes = (conf.public_interface4 and conf.route4) or + (conf.public_interface6 and conf.route46) + + local public_links = {} - config.app(c, "PublicDispatch", dispatch.PublicDispatch, { - node_ip4 = conf.public_interface.ip4 - }) config.app(c, "PublicRouter", route.PublicRouter, { sa = conf.inbound_sa }) - config.app(c, "PublicICMP4", icmp.ICMP4, { - node_ip4 = conf.public_interface.ip4 - }) - config.app(c, "PublicNextHop", nexthop.NextHop4, { - node_mac = conf.public_interface.mac, - node_ip4 = conf.public_interface.ip4, - nexthop_ip4 = conf.public_interface.nexthop_ip4, - nexthop_mac = conf.public_interface.nexthop_mac - }) - 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") + + if conf.public_interface4 then + config.app(c, "PublicDispatch", dispatch.PublicDispatch, { + node_ip4 = interface.ip4 + }) + config.app(c, "PublicICMP4", icmp.ICMP4, { + node_ip4 = interface.ip4 + }) + config.app(c, "PublicNextHop", nexthop.NextHop4, { + node_mac = interface.mac, + node_ip4 = interface.ip4, + nexthop_ip4 = interface.nexthop_ip4, + nexthop_mac = interface.nexthop_mac + }) + 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") + public_links.input = "PublicDispatch.input" + public_links.output = "PublicNextHop.output" + + elseif conf.public_interface6 then + config.app(c, "PublicDispatch", dispatch.PublicDispatch, { + node_ip6 = interface.ip6 + }) + config.app(c, "PublicICMP6", icmp.ICMP6, { + node_ip6 = interface.ip6 + }) + config.app(c, "PublicNextHop", Join) + config.app(c, "PublicND", nd_light, { + local_mac = interface.mac, + local_ip = interface.ip6, + next_hop = interface.nexthop_ip6, + remote_mac = interface.nexthop_mac + }) + config.link(c, "PublicDispatch.forward6 -> PublicRouter.input") + config.link(c, "PublicDispatch.icmp6 -> PublicICMP6.input") + config.link(c, "PublicDispatch.nd -> PublicND.south") + config.link(c, "PublicDispatch.protocol6_unreachable -> PublicICMP6.protocol_unreachable") + config.link(c, "PublicICMP6.output -> PublicNextHop.icmp6") + config.link(c, "PublicNextHop.output -> PublicND.north") + public_links.input = "PublicDispatch.input" + public_links.output = "PublicND.south" + end if not conf.data_plane then config.app(c, "Protocol_in_Tx", Transmitter, "Protocol_in") @@ -311,13 +360,18 @@ function configure_public_router (conf, append) config.link(c, "Protocol_out_Rx.output -> PublicNextHop.protocol") end - for id, route in pairs(conf.route) do + for id, route in pairs(routes) do local public_out = "PublicNextHop."..id local ESP_out = "ESP_"..id.."_out" local Tunnel = "Tunnel_"..id config.app(c, ESP_out.."_Rx", Receiver, ESP_out) - config.app(c, Tunnel, tunnel.Tunnel4, - {src=conf.public_interface.ip4, dst=route.gw_ip4}) + if route.gw_ip4 then + config.app(c, Tunnel, tunnel.Tunnel4, + {src=interface.ip4, dst=route.gw_ip4}) + elseif route.gw_ip6 then + config.app(c, Tunnel, tunnel.Tunnel6, + {src=interface.ip6, dst=route.gw_ip6}) + end config.link(c, ESP_out.."_Rx.output -> "..Tunnel..".input") config.link(c, Tunnel..".output -> "..public_out) end @@ -329,11 +383,6 @@ function configure_public_router (conf, append) config.link(c, public_in.." -> "..DSP_in.."_Tx.input") end - local public_links = { - input = "PublicDispatch.input", - output = "PublicNextHop.output" - } - return c, public_links end @@ -380,11 +429,16 @@ function configure_exchange (conf, append) if conf.data_plane then return end - if not conf.public_interface then return c end + local interface = conf.public_interface4 or conf.public_interface6 + if not interface then return c end + + local routes = (conf.public_interface4 and conf.route4) or + (conf.public_interface6 and conf.route46) config.app(c, "KeyExchange", exchange.KeyManager, { - node_ip4 = conf.public_interface.ip4, - routes = conf.route, + node_ip4 = interface.ip4, + node_ip6 = interface.ip6, + routes = routes, sa_db_path = sa_db_path, negotiation_ttl = conf.negotiation_ttl, sa_ttl = conf.sa_ttl