summaryrefslogtreecommitdiffstats
path: root/nselib/pppoe.lua
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--nselib/pppoe.lua1010
1 files changed, 1010 insertions, 0 deletions
diff --git a/nselib/pppoe.lua b/nselib/pppoe.lua
new file mode 100644
index 0000000..b092781
--- /dev/null
+++ b/nselib/pppoe.lua
@@ -0,0 +1,1010 @@
+--- A minimalistic PPPoE (Point-to-point protocol over Ethernet)
+-- library, implementing basic support for PPPoE
+-- Discovery and Configuration requests. The PPPoE protocol is ethernet based
+-- and hence does not use any IPs or port numbers.
+--
+-- The library contains a number of classes to support packet creation,
+-- parsing and sending/receiving responses. The classes are:
+-- o LCP - Contains classes to build and parse PPPoE LCP requests and
+-- responses.
+--
+-- o PPPoE - Contains classes to build and parse PPPoE requests and
+-- responses.
+--
+-- o Comm - Contains some basic functions for sending and receiving
+-- LCP and PPPoE requests and responses.
+--
+-- o Helper- The Helper class serves as the main interface between scripts
+-- and the library.
+--
+--
+-- @author Patrik Karlsson <patrik@cqure.net>
+--
+
+local rand = require "rand"
+local nmap = require "nmap"
+local packet = require "packet"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+_ENV = stdnse.module("pppoe", stdnse.seeall)
+
+
+EtherType = {
+ PPPOE_DISCOVERY = 0x8863,
+ PPPOE_SESSION = 0x8864,
+}
+
+-- A Class to handle the Link Control Protocol LCP
+LCP = {
+
+ ConfigOption = {
+
+ RESERVED = 0,
+ MRU = 1,
+ AUTH_PROTO = 3,
+ QUAL_PROTO = 4,
+ MAGIC_NUMBER = 5,
+ PROTO_COMPR = 7,
+ ACFC = 8,
+
+ -- Value has already been encoded, treat it as a byte stream
+ RAW = -1,
+
+ -- Creates a new config option
+ -- @param option number containing the configuration option
+ -- @param value containing the configuration option value
+ -- @param raw string containing the configuration options raw value
+ -- @return o new instance of ConfigOption
+ new = function(self, option, value, raw)
+ local o = {
+ option = option,
+ value = value,
+ raw = raw,
+ }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- Parses a byte stream and builds a new instance of the ConfigOption
+ -- class
+ -- @param data string containing raw bytes to parse
+ -- @return o instance of ConfigOption
+ parse = function(data)
+ local opt, pos, len = {}, 1, 0
+ opt.option, len, pos = string.unpack("BB", data, pos)
+ opt.raw, pos = string.unpack("c" .. ( len - 2 ), data, pos)
+
+ -- MRU
+ if ( 1 == opt.option ) then
+ opt.value = string.unpack(">I2", opt.raw)
+ end
+ return LCP.ConfigOption:new(opt.option, opt.value, opt.raw)
+ end,
+
+ -- Converts the class instance to string
+ -- @return string containing the raw config option
+ __tostring = function(self)
+ -- MRU
+ if ( self.raw ) then
+ return string.pack(">BB", self.option, #self.raw + 2) .. self.raw
+ elseif( 1 == self.option ) then
+ return string.pack(">BBI2", 1, 4, self.value)
+ else
+ error( ("Unsupported configuration option %d"):format(self.option) )
+ end
+ end,
+ },
+
+ -- A class to hold multiple ordered config options
+ ConfigOptions = {
+
+ new = function(self, options)
+ local o = {
+ options = options or {},
+ }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- Adds a new config option to the table
+ -- @param option instance of ConfigOption
+ add = function(self, option)
+ table.insert(self.options, option)
+ end,
+
+ -- Gets a config option by ID
+ -- @param opt number containing the configuration option to retrieve
+ -- @return v instance of ConfigOption
+ getById = function(self, opt)
+ for _, v in ipairs(self.options) do
+ if ( v.option == opt ) then
+ return v
+ end
+ end
+ end,
+
+ -- Returns all config options in an ordered table
+ -- @return tab table containing all configuration options
+ getTable = function(self)
+ local tab = {}
+ for _, v in ipairs(self.options) do
+ table.insert(tab, v)
+ end
+ return tab
+ end,
+
+
+ -- Parses a byte stream and builds a new instance of the ConfigOptions
+ -- class
+ -- @param data string containing raw bytes to parse
+ -- @return o instance of ConfigOption
+ parse = function(data)
+ local options = LCP.ConfigOptions:new()
+ local pos, opt, opt_val, len
+
+ repeat
+ opt, len, pos = string.unpack(">BB", data, pos)
+ if ( 0 == opt ) then break end
+ opt_val, pos = string.unpack("c"..len, data, (pos - 2))
+ options:add(LCP.ConfigOption.parse(opt_val))
+ until( pos == #data )
+ return options
+ end,
+
+ -- Converts the class instance to string
+ -- @return string containing the raw config option
+ __tostring = function(self)
+ local str = ""
+ for _, v in ipairs(self.options) do
+ str = str .. tostring(v)
+ end
+ return str
+ end,
+
+ },
+
+ ConfigOptionName = {
+ [0] = "Reserved",
+ [1] = "Maximum receive unit",
+ [3] = "Authentication protocol",
+ [4] = "Quality protocol",
+ [5] = "Magic number",
+ [7] = "Protocol field compression",
+ [8] = "Address and control field compression",
+ },
+
+ Code = {
+ CONFIG_REQUEST = 1,
+ CONFIG_ACK = 2,
+ CONFIG_NAK = 3,
+ TERMINATE_REQUEST = 5,
+ TERMINATE_ACK = 6,
+ },
+
+ -- The LCP Header
+ Header = {
+
+ -- Creates a new instance of the LCP header
+ -- @param code number containing the LCP code of the request
+ -- @param identifier number containing the LCP identifier
+ new = function(self, code, identifier)
+ local o = {
+ code = code,
+ identifier = identifier or 1,
+ length = 0,
+ }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+
+ -- Parses a byte stream and builds a new instance of the Header class
+ -- @param data string containing raw bytes to parse
+ -- @return o instance of ConfigOption
+ parse = function(data)
+ local header = LCP.Header:new()
+ header.code, header.identifier, header.length = string.unpack(">BBI2", data)
+ return header
+ end,
+
+ -- Converts the class instance to string
+ -- @return string containing the raw config option
+ __tostring = function(self)
+ return string.pack(">BBI2", self.code, self.identifier, self.length)
+ end,
+
+ },
+
+ ConfigRequest = {
+
+ -- Creates a new instance of the ConfigRequest class
+ -- @param identifier number containing the LCP identifier
+ -- @param options table of <code>LCP.ConfigOption</code> options
+ -- @return o instance of ConfigRequest
+ new = function(self, identifier, options)
+ local o = {
+ header = LCP.Header:new(LCP.Code.CONFIG_REQUEST, identifier),
+ options = LCP.ConfigOptions:new(options)
+ }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- Parses a byte stream and builds a new instance of the ConfigRequest
+ -- class
+ -- @param data string containing raw bytes to parse
+ -- @return o instance of ConfigRequest
+ parse = function(data)
+ local req = LCP.ConfigRequest:new()
+ req.header = LCP.Header.parse(data)
+ req.options = LCP.ConfigOptions.parse(data:sub(#tostring(req.header) + 1))
+ return req
+ end,
+
+ -- Converts the class instance to string
+ -- @return string containing the raw config option
+ __tostring = function(self)
+ self.header.length = 4 + #tostring(self.options)
+ return tostring(self.header) .. tostring(self.options)
+ end,
+ },
+
+ ConfigNak = {
+
+ -- Creates a new instance of the ConfigNak class
+ -- @param identifier number containing the LCP identifier
+ -- @param options table of <code>LCP.ConfigOption</code> options
+ -- @return o instance of ConfigNak
+ new = function(self, identifier, options)
+ local o = {
+ header = LCP.Header:new(LCP.Code.CONFIG_NAK, identifier),
+ options = LCP.ConfigOptions:new(options),
+ }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- Converts the class instance to string
+ -- @return string containing the raw config option
+ __tostring = function(self)
+ self.header.length = 4 + #tostring(self.options)
+ return tostring(self.header) .. tostring(self.options)
+ end,
+ },
+
+ ConfigAck = {
+
+ -- Creates a new instance of the ConfigAck class
+ -- @param identifier number containing the LCP identifier
+ -- @param options table of <code>LCP.ConfigOption</code> options
+ -- @return o instance of ConfigNak
+ new = function(self, identifier, options)
+ local o = {
+ header = LCP.Header:new(LCP.Code.CONFIG_ACK, identifier),
+ options = LCP.ConfigOptions:new(options),
+ }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- Parses a byte stream and builds a new instance of the ConfigAck class
+ -- @param data string containing raw bytes to parse
+ -- @return o instance of ConfigRequest
+ parse = function(data)
+ local ack = LCP.ConfigAck:new()
+ ack.header = LCP.Header.parse(data)
+ ack.options = LCP.ConfigOptions.parse(data:sub(#tostring(ack.header) + 1))
+ return ack
+ end,
+
+ -- Converts the class instance to string
+ -- @return string containing the raw config option
+ __tostring = function(self)
+ self.header.length = 4 + #tostring(self.options)
+ return tostring(self.header) .. tostring(self.options)
+ end,
+
+ },
+
+ TerminateRequest = {
+
+ -- Creates a new instance of the TerminateRequest class
+ -- @param identifier number containing the LCP identifier
+ -- @return o instance of ConfigNak
+ new = function(self, identifier, data)
+ local o = {
+ header = LCP.Header:new(LCP.Code.TERMINATE_REQUEST, identifier),
+ data = data or "",
+ }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- Converts the class instance to string
+ -- @return string containing the raw config option
+ __tostring = function(self)
+ self.header.length = 4 + #self.data
+ return tostring(self.header) .. self.data
+ end,
+ }
+
+}
+
+-- The PPPoE class
+PPPoE = {
+
+ -- Supported PPPoE codes (requests/responses)
+ Code = {
+ SESSION_DATA = 0x00,
+ PADO = 0x07,
+ PADI = 0x09,
+ PADR = 0x19,
+ PADS = 0x65,
+ PADT = 0xa7,
+ },
+
+ -- Support PPPoE Tag types
+ TagType = {
+ SERVICE_NAME = 0x0101,
+ AC_NAME = 0x0102,
+ HOST_UNIQUE = 0x0103,
+ AC_COOKIE = 0x0104,
+ },
+
+ -- Table used to convert table IDs to Names
+ TagName = {
+ [0x0101] = "Service-Name",
+ [0x0102] = "AC-Name",
+ [0x0103] = "Host-Uniq",
+ [0x0104] = "AC-Cookie",
+ },
+
+
+ Header = {
+
+ -- Creates a new instance of the PPPoE header class
+ -- @param code number containing the PPPoE code
+ -- @param session number containing the PPPoE session
+ -- @return o instance of Header
+ new = function(self, code, session)
+ local o = {
+ version = 1,
+ type = 1,
+ code = code,
+ session = session or 0,
+ length = 0,
+ }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- Parses a byte stream and builds a new instance of the class
+ -- @param data string containing raw bytes to parse
+ -- @return o instance of Header
+ parse = function(data)
+ local header = PPPoE.Header:new()
+ local vertyp
+ vertyp, header.code, header.session, header.length = string.unpack(">BBI2I2", data)
+ header.version = (vertyp >> 4)
+ header.type = (vertyp & 0x0F)
+ return header
+ end,
+
+ -- Converts the instance to string
+ -- @return string containing the raw config option
+ __tostring = function(self)
+ local vertype = (self.version << 4) + self.type
+ return string.pack(">BBI2I2", vertype, self.code, self.session, self.length)
+ end,
+
+
+ },
+
+ -- The TAG NVP Class
+ Tag = {
+
+ -- Creates a new instance of the Tag class
+ -- @param tag number containing the tag type
+ -- @param value string/number containing the tag value
+ -- @return o instance of Tag
+ new = function(self, tag, value)
+ local o = { tag = tag, value = value or "" }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- Converts the instance to string
+ -- @return string containing the raw config option
+ __tostring = function(self)
+ return string.pack(">I2s2", self.tag, self.value)
+ end,
+ },
+
+ PADI = {
+
+ -- Creates a new instance of the PADI class
+ -- @param tags table of <code>PPPoE.Tag</code> instances
+ -- @param value string/number containing the tag value
+ -- @return o instance of ConfigNak
+ new = function(self, tags)
+ local c = rand.random_string(8)
+
+ local o = {
+ header = PPPoE.Header:new(PPPoE.Code.PADI),
+ tags = tags or {
+ PPPoE.Tag:new(PPPoE.TagType.SERVICE_NAME),
+ PPPoE.Tag:new(PPPoE.TagType.HOST_UNIQUE, c)
+ }
+ }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- Converts the instance to string
+ -- @return string containing the raw config option
+ __tostring = function(self)
+ local tags = ""
+ for _, tag in ipairs(self.tags) do
+ tags = tags .. tostring(tag)
+ end
+ self.header.length = #tags
+ return tostring(self.header) .. tags
+ end,
+
+ },
+
+ PADO = {
+
+ -- Creates a new instance of the PADO class
+ -- @return o instance of PADO
+ new = function(self)
+ local o = { tags = {} }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- Parses a byte stream and builds a new instance of the class
+ -- @param data string containing raw bytes to parse
+ -- @return o instance of PADO
+ parse = function(data)
+ local pado = PPPoE.PADO:new()
+ pado.header = PPPoE.Header.parse(data)
+ local pos = #tostring(pado.header) + 1
+ pado.data = data:sub(pos)
+
+ repeat
+ local tag, decoded, raw
+ tag, raw, pos = string.unpack(">I2s2", pos)
+ if ( PPPoE.TagDecoder[tag] ) then
+ decoded = PPPoE.TagDecoder[tag](raw)
+ else
+ stdnse.debug1("PPPoE: Unsupported tag (%d)", tag)
+ end
+ local t = PPPoE.Tag:new(tag, raw)
+ t.decoded = decoded
+ table.insert(pado.tags, t)
+ until( pos >= #data )
+
+ return pado
+ end,
+ },
+
+ PADR = {
+
+ -- Creates a new instance of the PADR class
+ -- @param tags table of <code>PPPoE.Tag</code> instances
+ -- @return o instance of PADR
+ new = function(self, tags)
+ local o = {
+ tags = tags or {},
+ header = PPPoE.Header:new(PPPoE.Code.PADR)
+ }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- Converts the instance to string
+ -- @return string containing the raw config option
+ __tostring = function(self)
+ local tags = ""
+ for _, tag in ipairs(self.tags) do
+ tags = tags .. tostring(tag)
+ end
+ self.header.length = #tags
+ return tostring(self.header) .. tags
+ end,
+
+ },
+
+ PADS = {
+
+ -- Creates a new instance of the PADS class
+ -- @return o instance of PADS
+ new = function(self)
+ local o = { tags = {} }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- Parses a byte stream and builds a new instance of the class
+ -- @param data string containing raw bytes to parse
+ -- @return o instance of PADS
+ parse = function(data)
+ local pads = PPPoE.PADS:new()
+ pads.header = PPPoE.Header.parse(data)
+ local pos = #tostring(pads.header) + 1
+ pads.data = data:sub(pos)
+ return pads
+ end,
+
+ },
+
+ PADT = {
+
+ -- Creates a new instance of the PADT class
+ -- @param session number containing the PPPoE session
+ -- @return o instance of PADT
+ new = function(self, session)
+ local o = { header = PPPoE.Header:new(PPPoE.Code.PADT) }
+ setmetatable(o, self)
+ o.header.session = session
+ self.__index = self
+ return o
+ end,
+
+ -- Parses a byte stream and builds a new instance of the class
+ -- @param data string containing raw bytes to parse
+ -- @return o instance of PADI
+ parse = function(data)
+ local padt = PPPoE.PADT:new()
+ padt.header = PPPoE.Header.parse(data)
+ return padt
+ end,
+
+ -- Converts the instance to string
+ -- @return string containing the raw config option
+ __tostring = function(self)
+ return tostring(self.header)
+ end,
+ },
+
+ SessionData = {
+
+ -- Creates a new instance of the SessionData class
+ -- @param session number containing the PPPoE session
+ -- @param data string containing the LCP data to send
+ -- @return o instance of ConfigNak
+ new = function(self, session, data)
+ local o = {
+ data = data or "",
+ header = PPPoE.Header:new(PPPoE.Code.SESSION_DATA)
+ }
+ setmetatable(o, self)
+ o.header.session = session
+ self.__index = self
+ return o
+ end,
+
+ -- Parses a byte stream and builds a new instance of the class
+ -- @param data string containing raw bytes to parse
+ -- @return o instance of SessionData
+ parse = function(data)
+ local sess = PPPoE.SessionData:new()
+ sess.header = PPPoE.Header.parse(data)
+ local pos = #tostring(sess.header) + 1 + 2
+ sess.data = data:sub(pos)
+ return sess
+ end,
+
+ -- Converts the instance to string
+ -- @return string containing the raw config option
+ __tostring = function(self)
+ -- 2 for the encapsulation
+ self.header.length = 2 + 4 + #self.data
+ return tostring(self.header) .. "\xC0\x21" .. self.data
+ end,
+
+ }
+
+
+}
+
+-- A bunch of tag decoders
+PPPoE.TagDecoder = {}
+PPPoE.TagDecoder.decodeHex = stdnse.tohex
+PPPoE.TagDecoder.decodeStr = function(data) return data end
+PPPoE.TagDecoder[PPPoE.TagType.SERVICE_NAME] = PPPoE.TagDecoder.decodeStr
+PPPoE.TagDecoder[PPPoE.TagType.AC_NAME] = PPPoE.TagDecoder.decodeStr
+PPPoE.TagDecoder[PPPoE.TagType.AC_COOKIE] = PPPoE.TagDecoder.decodeHex
+PPPoE.TagDecoder[PPPoE.TagType.HOST_UNIQUE] = PPPoE.TagDecoder.decodeHex
+
+-- The Comm class responsible for communication with the PPPoE server
+Comm = {
+
+ -- Creates a new instance of the Comm class
+ -- @param iface string containing the interface name
+ -- @param src_mac string containing the source MAC address
+ -- @param dst_mac string containing the destination MAC address
+ -- @return o new instance of Comm
+ new = function(self, iface, src_mac, dst_mac)
+ local o = {
+ iface = iface,
+ src_mac = src_mac,
+ dst_mac = dst_mac,
+ }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- Sets up the pcap receiving socket
+ -- @return status true on success
+ connect = function(self)
+ self.socket = nmap.new_socket()
+ self.socket:set_timeout(10000)
+
+ local mac = stdnse.format_mac(self.src_mac)
+
+ -- let's set a filter on PPPoE we can then check what packet is ours,
+ -- based on the HOST_UNIQUE tag, if we need to
+ self.socket:pcap_open(self.iface, 1500, false, "ether[0x0c:2] == 0x8863 or ether[0x0c:2] == 0x8864 and ether dst " .. mac)
+ return true
+ end,
+
+ -- Sends a packet
+ -- @param data class containing the request to send
+ -- @return status true on success, false on failure
+ send = function(self, data)
+ local eth_type = ( data.header.code == PPPoE.Code.SESSION_DATA ) and 0x8864 or 0x8863
+ local ether = self.dst_mac .. self.src_mac .. string.pack(">I2", eth_type)
+ local p = packet.Frame:new(ether .. tostring(data))
+
+ local sock = nmap.new_dnet()
+ if ( not(sock) ) then
+ return false, "Failed to create raw socket"
+ end
+
+ local status = sock:ethernet_open(self.iface)
+ -- we don't actually need to do this as the script simply crashes
+ -- if we don't have the right permissions at this point
+ if ( not(status) ) then
+ return false, "Failed to open raw socket"
+ end
+
+ status = sock:ethernet_send(p.frame_buf)
+ if ( not(status) ) then
+ return false, "Failed to send data"
+ end
+ sock:ethernet_close()
+ return true
+ end,
+
+ -- Receive a response from the server
+ -- @return status true on success, false on failure
+ -- @return response class containing the response or
+ -- err string on error
+ recv = function(self)
+ local status, _, l2, l3 = self.socket:pcap_receive()
+ -- if we got no response, just return false as there's
+ -- probably not really an error
+ if ( not(status) ) then
+ return false, "Did not receive any packets"
+ end
+
+ local header = PPPoE.Header.parse(l3)
+ local p = packet.Frame:new(l2..l3)
+
+ -- there's probably a more elegant way of doing this
+ if ( EtherType.PPPOE_DISCOVERY == p.ether_type ) then
+ if ( header.code == PPPoE.Code.PADO ) then
+ local pado = PPPoE.PADO.parse(l3)
+ pado.mac_srv = p.mac_src
+ return true, pado
+ elseif ( header.code == PPPoE.Code.PADS ) then
+ local pads = PPPoE.PADS.parse(l3)
+ return true, pads
+ elseif ( header.code == PPPoE.Code.PADT ) then
+ local pads = PPPoE.PADT.parse(l3)
+ return true, pads
+ end
+ elseif ( EtherType.PPPOE_SESSION == p.ether_type ) then
+ return true, PPPoE.SessionData.parse(l3)
+ end
+ return false, ("Received unsupported response, can't decode code (%d)"):format(header.code)
+ end,
+
+ -- Does an "exchange", ie, sends a request and waits for a response
+ -- @param data class containing the request to send
+ -- @return status true on success, false on failure
+ -- @return response class containing the response or
+ -- err string on error
+ exch = function(self, data)
+ local status, err = self:send(data)
+ if ( not(status) ) then
+ return false, err
+ end
+ local retries, resp = 3, nil
+
+ repeat
+ status, resp = self:recv()
+ if ( data.header and 0 == data.header.session ) then
+ return true, resp
+ elseif ( data.header and data.header.session == resp.header.session ) then
+ return true, resp
+ end
+ retries = retries - 1
+ until(retries == 0)
+
+ return false, "Failed to retrieve proper PPPoE response"
+ end,
+
+}
+
+-- The Helper class is the main script interface
+Helper = {
+
+ -- Creates a new instance of Helper
+ -- @param iface string containing the name of the interface to use
+ -- @return o new instance on success, nil on failure
+ new = function(self, iface)
+ local o = {
+ iface = iface,
+
+ -- set the LCP identifier to 0
+ identifier = 0,
+ }
+ setmetatable(o, self)
+ self.__index = self
+
+ if ( not(nmap.is_privileged()) ) then
+ return nil, "The PPPoE library requires Nmap to be run in privileged mode"
+ end
+
+ -- get src_mac
+ local info = nmap.get_interface_info(iface)
+ if ( not(info) or not(info.mac) ) then
+ return nil, "Failed to get source MAC address"
+ end
+ o.comm = Comm:new(iface, info.mac)
+ return o
+ end,
+
+ -- Sets up the pcap socket for listening and does some other preparations
+ -- @return status true on success, false on failure
+ connect = function(self)
+ return self.comm:connect()
+ end,
+
+
+ -- Performs a PPPoE discovery initiation by sending a PADI request to the
+ -- ethernet broadcast address
+ -- @return status true on success, false on failure
+ -- @return pado instance of PADO on success, err string on failure
+ discoverInit = function(self)
+ local padi = PPPoE.PADI:new()
+ self.comm.dst_mac = ("\xFF"):rep(6)
+ local status, err = self.comm:send(padi)
+ if ( not(status) ) then
+ return false, err
+ end
+ -- wait for a pado
+ local pado, retries = nil, 3
+
+ repeat
+ status, pado = self.comm:recv()
+ if ( not(status) ) then
+ return status, pado
+ end
+ retries = retries - 1
+ until( pado.tags or retries == 0 )
+ if ( not(pado.tags) ) then
+ return false, "PADO response contained no tags"
+ end
+
+ local pado_host_unique
+ for _, tag in ipairs(pado.tags) do
+ if ( PPPoE.TagType.HOST_UNIQUE == tag.tag ) then
+ pado_host_unique = tag.raw
+ end
+ end
+
+ -- store the tags for later use
+ self.tags = pado.tags
+ self.comm.dst_mac = pado.mac_srv
+
+ if ( pado_host_unique and
+ pado_host_unique ~= padi.tags[PPPoE.TagType.HOST_UNIQUE] ) then
+ -- currently, we don't handle this, we probably should
+ -- in order to do so, we need to split the function exch
+ -- to recv and send
+ return false, "Got incorrect answer"
+ end
+
+ return true, pado
+ end,
+
+ -- Performs a Discovery Request by sending PADR to the PPPoE ethernet
+ -- address
+ -- @return status true on success, false on failure
+ -- @return pads instance of PADS on success
+ discoverRequest = function(self)
+
+ -- remove the AC-Name tag if there is one
+ local function getTag(tag)
+ for _, t in ipairs(self.tags) do
+ if ( tag == t.tag ) then
+ return t
+ end
+ end
+ end
+
+ local taglist = {
+ PPPoE.TagType.SERVICE_NAME,
+ PPPoE.TagType.HOST_UNIQUE,
+ PPPoE.TagType.AC_COOKIE
+ }
+
+ local tags = {}
+ for _, t in ipairs(taglist) do
+ if ( getTag(t) ) then
+ table.insert(tags, getTag(t))
+ end
+ end
+
+ local padr = PPPoE.PADR:new(tags)
+ local status, pads = self.comm:exch(padr)
+
+ if ( status ) then
+ self.session = pads.header.session
+ end
+
+ return status, pads
+ end,
+
+ -- Attempts to specify a method for authentication
+ -- If the server responds with another method it's NAK:ed and we try to set
+ -- our requested method instead. If this fails, we return a failure
+ -- @param method string containing one of the following methods:
+ -- <code>MSCHAPv1</code>, <code>MSCHAPv2</code> or <code>PAP</code>
+ -- @return status true on success, false on failure
+ -- err string containing error message on failure
+ setAuthMethod = function(self, method)
+
+ local AuthMethod = {
+ methods = {
+ { name = "EAP", value = "\xC2\x27" },
+ { name = "MSCHAPv1", value = "\xC2\x23\x80" },
+ { name = "MSCHAPv2", value = "\xC2\x23\x81" },
+ { name = "PAP", value = "\xC0\x23" },
+ }
+ }
+
+ AuthMethod.byName = function(name)
+ for _, m in ipairs(AuthMethod.methods) do
+ if ( m.name == name ) then
+ return m
+ end
+ end
+ end
+
+ AuthMethod.byValue = function(value)
+ for _, m in ipairs(AuthMethod.methods) do
+ if ( m.value == value ) then
+ return m
+ end
+ end
+ end
+
+ local auth_data = ( AuthMethod.byName(method) and AuthMethod.byName(method).value )
+ if ( not(auth_data) ) then
+ return false, ("Unsupported authentication mode (%s)"):format(method)
+ end
+
+ self.identifier = self.identifier + 1
+
+ -- First do a Configuration Request
+ local options = { LCP.ConfigOption:new(LCP.ConfigOption.MRU, 1492) }
+ local lcp_req = LCP.ConfigRequest:new(self.identifier, options)
+ local sess_req = PPPoE.SessionData:new(self.session, tostring(lcp_req))
+ local status, resp = self.comm:exch(sess_req)
+
+ if ( not(status) or PPPoE.Code.SESSION_DATA ~= resp.header.code ) then
+ return false, "Unexpected packet type was received"
+ end
+
+ -- Make sure we got a Configuration Request in return
+ local lcp_header = LCP.Header.parse(resp.data)
+ if ( LCP.Code.CONFIG_REQUEST ~= lcp_header.code ) then
+ return false, ("Unexpected packet type was received (%d)"):format(lcp_header.code)
+ end
+
+ local config_req = LCP.ConfigRequest.parse(resp.data)
+ if ( not(config_req.options) ) then
+ return false, "Failed to retrieve any options from response"
+ end
+
+ local auth_proposed = config_req.options:getById(LCP.ConfigOption.AUTH_PROTO)
+
+ if ( auth_proposed.raw ~= auth_data ) then
+ local options = { LCP.ConfigOption:new(LCP.ConfigOption.AUTH_PROTO, nil, auth_data) }
+ local lcp_req = LCP.ConfigNak:new(self.identifier, options)
+ local sess_req = PPPoE.SessionData:new(self.session, tostring(lcp_req))
+ local status, resp = self.comm:exch(sess_req)
+
+ if ( not(status) or PPPoE.Code.SESSION_DATA ~= resp.header.code ) then
+ return false, "Unexpected packet type was received"
+ end
+
+ -- Make sure we got a Configuration Request in return
+ local lcp_header = LCP.Header.parse(resp.data)
+ if ( LCP.Code.CONFIG_REQUEST ~= lcp_header.code ) then
+ return false, ("Unexpected packet type was received (%d)"):format(lcp_header.code)
+ end
+
+ config_req = LCP.ConfigRequest.parse(resp.data)
+
+ -- if the authentication methods match, send an ACK
+ if ( config_req.options:getById(LCP.ConfigOption.AUTH_PROTO).raw == auth_data ) then
+ -- The ACK is essential the Config Request, only with a different code
+ -- Do a dirty attempt to just replace the code and send the request back as an ack
+ self.identifier = self.identifier + 1
+
+ local lcp_req = LCP.ConfigAck:new(config_req.header.identifier, config_req.options:getTable())
+ local sess_req = PPPoE.SessionData:new(self.session, tostring(lcp_req))
+ local status, resp = self.comm:send(sess_req)
+
+ return true
+ end
+
+ return false, "Authentication method was not accepted"
+ end
+
+
+ return false, "Failed to negotiate authentication mechanism"
+ end,
+
+ -- Sends a LCP Terminate Request and waits for an ACK
+ -- Attempts to do so 10 times before aborting
+ -- @return status true on success false on failure
+ close = function(self)
+ local tries = 10
+ repeat
+ if ( 0 == self.session ) then
+ break
+ end
+ local lcp_req = LCP.TerminateRequest:new(self.identifier)
+ local sess_req = PPPoE.SessionData:new(self.session, tostring(lcp_req))
+ local status, resp = self.comm:exch(sess_req)
+ if ( status and resp.header and resp.header.code ) then
+ if ( PPPoE.Code.SESSION_DATA == resp.header.code ) then
+ local lcp_header = LCP.Header.parse(resp.data)
+ if ( LCP.Code.TERMINATE_ACK == lcp_header.code ) then
+ break
+ end
+ end
+ end
+ tries = tries - 1
+ until( tries == 0 )
+
+ self.comm:exch(PPPoE.PADT:new(self.session))
+
+ return true
+ end,
+
+}
+
+return _ENV;