1179 lines
46 KiB
Python
1179 lines
46 KiB
Python
# −*− coding: UTF−8 −*−
|
||
#/**
|
||
# * Software Name : pycrate
|
||
# * Version : 0.4
|
||
# *
|
||
# * Copyright 2013. Benoit Michau. ANSSI.
|
||
# *
|
||
# * This library is free software; you can redistribute it and/or
|
||
# * modify it under the terms of the GNU Lesser General Public
|
||
# * License as published by the Free Software Foundation; either
|
||
# * version 2.1 of the License, or (at your option) any later version.
|
||
# *
|
||
# * This library is distributed in the hope that it will be useful,
|
||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||
# * Lesser General Public License for more details.
|
||
# *
|
||
# * You should have received a copy of the GNU Lesser General Public
|
||
# * License along with this library; if not, write to the Free Software
|
||
# * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||
# * MA 02110-1301 USA
|
||
# *
|
||
# *--------------------------------------------------------
|
||
# * File Name : pycrate_corenet/ServerGTPU.py
|
||
# * Created : 2013-11-04
|
||
# * Authors : Benoit Michau
|
||
# *--------------------------------------------------------
|
||
#*/
|
||
|
||
#------------------------------------------------------------------------------#
|
||
# GTP-U handler works with Linux PF_PACKET RAW socket on the Internet side
|
||
# and with standard GTP-U 3GPP protocol on the RNC / eNB side
|
||
# RNC / eNB <== [IP/UDP/GTPU/IP_mobile] ==> GTPUd <== [RawEthernet/IP_mobile] ==> Internet
|
||
#
|
||
# This way, the complete IP interface of a mobile is exposed through this Gi interface.
|
||
# It requires the GTPUd to resolve ARP request on behalf of mobiles that it handles:
|
||
# this is the role of ARPd
|
||
#------------------------------------------------------------------------------#
|
||
|
||
# filtering exports
|
||
__all__ = ['ARPd', 'GTPUd', 'DPI', 'MOD', 'DNSRESP', 'TCPSYNACK']
|
||
|
||
import os
|
||
import signal
|
||
#
|
||
if os.name != 'nt':
|
||
from fcntl import ioctl
|
||
from socket import timeout
|
||
from random import _urandom
|
||
else:
|
||
print('[ERR] ServerGTPU : you\'re not on *nix system. It\'s not going to work:\n'\
|
||
'You need PF_PACKET socket')
|
||
|
||
from pycrate_core.elt import Envelope
|
||
from pycrate_ether.IP import *
|
||
from .utils import *
|
||
|
||
#------------------------------------------------------------------------------#
|
||
# setting / unsetting ethernet IF in promiscuous mode #
|
||
#------------------------------------------------------------------------------#
|
||
# copied from scapy (scapy/scapy/arch/linux.py)
|
||
|
||
SIOCGIFINDEX = 0x8933 # name -> if_index mapping
|
||
SOL_PACKET = 263
|
||
PACKET_MR_PROMISC = 1
|
||
PACKET_ADD_MEMBERSHIP = 1
|
||
PACKET_DROP_MEMBERSHIP = 2
|
||
|
||
def get_if(iff, cmd):
|
||
"""Ease SIOCGIF* ioctl calls"""
|
||
sk = socket.socket()
|
||
ifreq = ioctl(sk, cmd, pack('16s16x', iff.encode('utf8')))
|
||
sk.close()
|
||
return ifreq
|
||
|
||
def get_if_index(iff):
|
||
return int(unpack('I', get_if(iff, SIOCGIFINDEX)[16:20])[0])
|
||
|
||
def set_promisc(sk, iff, val=1):
|
||
mreq = pack('IHH8s', get_if_index(iff), PACKET_MR_PROMISC, 0, b'')
|
||
if val:
|
||
cmd = PACKET_ADD_MEMBERSHIP
|
||
else:
|
||
cmd = PACKET_DROP_MEMBERSHIP
|
||
sk.setsockopt(SOL_PACKET, cmd, mreq)
|
||
|
||
|
||
#------------------------------------------------------------------------------#
|
||
# ARPd #
|
||
#------------------------------------------------------------------------------#
|
||
|
||
class ARPd(object):
|
||
'''
|
||
ARP resolver
|
||
resolves Ethernet / IPv4 address correspondence on behalf of UE connected over
|
||
GTP-U.
|
||
|
||
The method .resolve(ipaddr) returns the MAC address for the requested IP
|
||
address.
|
||
It runs a background thread too, that answers ARP requests on behalf of
|
||
connected mobiles.
|
||
|
||
When handling mobiles' network interfaces over GTP-U, the following steps
|
||
are followed:
|
||
- for outgoing packets:
|
||
1) for any destination IP outside of our network (e.g. 192.168.1.0/24),
|
||
provide the ROUTER_MAC_ADDR directly
|
||
2) for local destination IP address in our subnet,
|
||
provide the corresponding MAC address after an ARP resolution
|
||
- for incoming packets:
|
||
we must answer the router's or local hosts' ARP requests
|
||
before being able to receive IP packets to be transferred to the mobiles
|
||
|
||
ARPd:
|
||
maintains the ARP_RESOLV_TABLE
|
||
listens on the ethernet interface for:
|
||
- incoming ARP requests, and answer it for IP addresses from our IP_POOL
|
||
- incoming ARP responses (due to the daemon sending ARP requests)
|
||
- incoming IP packets (thanks to promiscous mode) to update the ARP_RESOLV_TABLE
|
||
with new MAC addresses opportunistically
|
||
sends ARP request when needed to be able then to forward IP packets from mobile
|
||
'''
|
||
#
|
||
# verbosity level: list of log types to display when calling
|
||
# self._log(logtype, msg)
|
||
DEBUG = ('ERR', 'WNG', 'INF', 'DBG')
|
||
#
|
||
# recv() buffer length
|
||
BUFLEN = 2048
|
||
# select() timeout and wait period
|
||
SELECT_TO = 0.1
|
||
SELECT_SLEEP = 0.05
|
||
#
|
||
# all Gi interface parameters
|
||
# Our GGSN ethernet parameters (IF, MAC and IP addresses)
|
||
# (and also the MAC address to be used for any mobiles through our GGSN)
|
||
GGSN_ETH_IF = 'eth0'
|
||
GGSN_MAC_ADDR = '08:00:00:01:02:03'
|
||
GGSN_IP_ADDR = '192.168.1.100'
|
||
#
|
||
# the set of IP address to be used by our mobiles
|
||
IP_POOL = {'192.168.1.201', '192.168.1.202', '192.168.1.203'}
|
||
#
|
||
# network parameters:
|
||
# subnet prefix
|
||
# WNG: we only handle IPv4 /24 subnet
|
||
SUBNET_PREFIX = '192.168.1.0/24'
|
||
# and 1st IP router (MAC and IP addresses)
|
||
# this is to resolve directly any IP outside our subnet
|
||
ROUTER_MAC_ADDR = 'f4:00:00:01:02:03'
|
||
ROUTER_IP_ADDR = '192.168.1.1'
|
||
#
|
||
CATCH_SIGINT = False
|
||
|
||
def __init__(self, opportunist=False):
|
||
#
|
||
self.GGSN_MAC_BUF = mac_aton(self.GGSN_MAC_ADDR)
|
||
self.GGSN_IP_BUF = inet_aton(self.GGSN_IP_ADDR)
|
||
self.ROUTER_MAC_BUF = mac_aton(self.ROUTER_MAC_ADDR)
|
||
self.ROUTER_IP_BUF = inet_aton(self.ROUTER_IP_ADDR)
|
||
# use an uint32 for the subnet prefix
|
||
prefip, prefmask = self.SUBNET_PREFIX.split('/')
|
||
pref = unpack('>I', inet_aton(prefip))[0]
|
||
self.SUBNET_MASK = (1<<32)-(1<<(32-int(prefmask)))
|
||
self.SUBNET_PREFIX = pref & self.SUBNET_MASK
|
||
#
|
||
# init RAW ethernet socket for ARP
|
||
self.sk_arp = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, ntohs(0x0806))
|
||
self.sk_arp.settimeout(0.1)
|
||
#self.sk_arp.setsockopt(SOL_PACKET, SO_RCVBUF, 0)
|
||
self.sk_arp.bind((self.GGSN_ETH_IF, 0x0806))
|
||
#self.sk_arp.setsockopt(SOL_PACKET, SO_RCVBUF, 2**24)
|
||
#
|
||
# init RAW ethernet socket for IPv4
|
||
self.sk_ip = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, ntohs(0x0800))
|
||
self.sk_ip.settimeout(0.1)
|
||
#self.sk_ip.setsockopt(SOL_PACKET, SO_RCVBUF, 0)
|
||
self.sk_ip.bind((self.GGSN_ETH_IF, 0x0800))
|
||
#self.sk_ip.setsockopt(SOL_PACKET, SO_RCVBUF, 2**24)
|
||
#
|
||
# ARP resolution table
|
||
self.ARP_RESOLV_TABLE = {
|
||
self.ROUTER_IP_ADDR : self.ROUTER_MAC_BUF,
|
||
self.GGSN_IP_ADDR : self.GGSN_MAC_BUF,
|
||
}
|
||
for ip in self.IP_POOL:
|
||
self.ARP_RESOLV_TABLE[ip] = self.GGSN_MAC_BUF
|
||
#
|
||
# interrupt handler
|
||
if self.CATCH_SIGINT:
|
||
def sigint_handler(signum, frame):
|
||
if self.DEBUG > 1:
|
||
self._log('INF', 'CTRL+C caught')
|
||
self.stop()
|
||
signal.signal(signal.SIGINT, sigint_handler)
|
||
#
|
||
self.set_opportunist(opportunist)
|
||
# starting main listening loop in background
|
||
self._listening = True
|
||
self._listener_t = threadit(self.listen)
|
||
self._log('INF', 'ARP resolver started')
|
||
#
|
||
# .resolve(ip) method is available for ARP resolution by GTPUd
|
||
|
||
def _log(self, logtype='DBG', msg=''):
|
||
# logtype: 'ERR', 'WNG', 'INF', 'DBG'
|
||
if logtype in self.DEBUG:
|
||
log('[%s] [ARPd] %s' % (logtype, msg))
|
||
|
||
def set_opportunist(self, state):
|
||
if state:
|
||
self.sk_list = (self.sk_arp, self.sk_ip)
|
||
else:
|
||
self.sk_list = (self.sk_arp, )
|
||
|
||
def stop(self):
|
||
if self._listening:
|
||
self._listening = False
|
||
sleep(self.SELECT_TO * 2)
|
||
try:
|
||
self.sk_arp.close()
|
||
self.sk_ip.close()
|
||
except Exception as err:
|
||
self._log('ERR', 'socket error: {0}'.format(err))
|
||
|
||
def listen(self):
|
||
# select() until we receive arp or ip packet
|
||
while self._listening:
|
||
r = []
|
||
r = select(self.sk_list, [], [], self.SELECT_TO)[0]
|
||
for sk in r:
|
||
try:
|
||
buf = sk.recvfrom(self.BUFLEN)[0]
|
||
except Exception as err:
|
||
self._log('ERR', 'external network error (recvfrom): %s' % err)
|
||
buf = b''
|
||
# dipatch ARP request / IP response
|
||
if sk != self.sk_arp:
|
||
# sk == self.sk_ip
|
||
if len(buf) >= 34 and buf[12:14] == b'\x08\x00':
|
||
self._process_ipbuf(buf)
|
||
else:
|
||
# sk == self.sk_arp
|
||
if len(buf) >= 42 and buf[12:14] == b'\x08\x06':
|
||
self._process_arpbuf(buf)
|
||
#
|
||
# if select() timeouts, take a little rest
|
||
if len(r) == 0:
|
||
sleep(self.SELECT_SLEEP)
|
||
self._log('INF', 'ARP resolver stopped')
|
||
|
||
def _process_arpbuf(self, buf):
|
||
# this is an ARP request or response:
|
||
arpop = ord(buf[21:22])
|
||
# 1) check if it requests for one of our IP
|
||
if arpop == 1:
|
||
ipreq = inet_ntoa(buf[38:42])
|
||
if ipreq in self.IP_POOL:
|
||
# reply to it with our MAC ADDR
|
||
try:
|
||
self.sk_arp.sendto(
|
||
b''.join((buf[6:12], self.GGSN_MAC_BUF, # Ethernet hdr
|
||
b'\x08\x06\0\x01\x08\0\x06\x04\0\x02',
|
||
self.GGSN_MAC_BUF, buf[38:42], # ARP sender
|
||
buf[6:12], buf[28:32], # ARP target
|
||
b'\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0')),
|
||
(self.GGSN_ETH_IF, 0x0806))
|
||
except Exception as err:
|
||
self._log('ERR', 'external network error (sendto) on ARP response: %s' % err)
|
||
else:
|
||
self._log('DBG', 'ARP response sent for IP: %s' % ipreq)
|
||
# 2) check if it responses something useful for us
|
||
elif arpop == 2:
|
||
ipres_buf = buf[28:32]
|
||
if unpack('>I', ipres_buf)[0] & self.SUBNET_MASK == self.SUBNET_PREFIX:
|
||
ipres = inet_ntoa(ipres_buf)
|
||
if ipres not in self.ARP_RESOLV_TABLE:
|
||
# WNG: no protection (at all) against ARP cache poisoning
|
||
self.ARP_RESOLV_TABLE[ipres] = buf[22:28]
|
||
self._log('DBG', 'got ARP response for new local IP: %s' % ipres)
|
||
|
||
def _process_ipbuf(self, buf):
|
||
# this is an random IPv4 packet incoming into our interface:
|
||
# check if src IP is in our subnet and not already resolved,
|
||
# then store the Ethernet MAC address
|
||
# this is an opportunistic behaviour and disabled by default
|
||
ipsrc_buf = buf[26:30]
|
||
if unpack('>I', ipsrc_buf)[0] & self.SUBNET_MASK == self.SUBNET_PREFIX:
|
||
ipsrc = inet_ntoa(ipsrc_buf)
|
||
if ipsrc not in self.ARP_RESOLV_TABLE:
|
||
# WNG: no protection (at all) against ARP cache poisoning
|
||
self.ARP_RESOLV_TABLE[ipsrc] = buf[6:12]
|
||
self._log('DBG', 'got MAC address from IPv4 packet for new local IP: %s' % ipsrc)
|
||
|
||
def resolve(self, ip):
|
||
# check if already resolved
|
||
if ip in self.ARP_RESOLV_TABLE:
|
||
return self.ARP_RESOLV_TABLE[ip]
|
||
# or outside our local network
|
||
ip_buf = inet_aton(ip)
|
||
if unpack('>i', ip_buf)[0] & self.SUBNET_MASK != self.SUBNET_PREFIX:
|
||
return self.ROUTER_MAC_BUF
|
||
# requesting an IP within our local LAN
|
||
# starting a resolution for it
|
||
else:
|
||
try:
|
||
self.sk_arp.sendto(
|
||
b''.join((self.ROUTER_MAC_BUF, self.GGSN_MAC_BUF, # Ethernet hdr
|
||
b'\x08\x06\0\x01\x08\0\x06\x04\0\x01',
|
||
self.GGSN_MAC_BUF, self.GGSN_IP_BUF, # ARP sender
|
||
b'\0\0\0\0\0\0', inet_aton(ip), # ARP target
|
||
b'\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0')),
|
||
(self.GGSN_ETH_IF, 0x0806))
|
||
except Exception as err:
|
||
self._log('ERR', 'external network error (sendto) on ARP request: %s' % err)
|
||
else:
|
||
self._log('DBG', 'ARP request sent for local IP: %s' % ip)
|
||
# wait for the answer
|
||
cnt = 0
|
||
while ip not in self.ARP_RESOLV_TABLE:
|
||
sleep(self.SELECT_SLEEP)
|
||
cnt += 1
|
||
if cnt >= 3:
|
||
break
|
||
if cnt < 3:
|
||
return self.ARP_RESOLV_TABLE[ip]
|
||
else:
|
||
return 6*b'\xFF' # LAN broadcast, maybe a bit strong !
|
||
|
||
|
||
#------------------------------------------------------------------------------#
|
||
# GTPUd #
|
||
#------------------------------------------------------------------------------#
|
||
|
||
BLACKHOLE_LAN = 0b01
|
||
BLACKHOLE_WAN = 0b10
|
||
IPV6_LOCAL_PREF = b'\xfe\x80\0\0\0\0\0\0'
|
||
|
||
class GTPUd(object):
|
||
'''
|
||
GTP-U forwarder
|
||
bridges Ethernet to GTP-U to handle IPv4v6 data traffic of connected UE.
|
||
|
||
This is to be instantiated as a unique handler for all GTP-U tunnels
|
||
in the corenet mobile core network.
|
||
To add GTP tunnel endpoints at will, for each mobile, use the methods:
|
||
.add_mobile(teid_ul, mobile_addr, ran_ip, teid_dl)
|
||
- teid_ul will be the key used to index the connection
|
||
- mobile_addr is a 2-tuple (addr_type, ip_addr)
|
||
addr_type: 1 for IPv4, 2 for IPv6, 3 for IPv4v6
|
||
in case of IPv6 address, it is possible to set only the 64 1st bits
|
||
(the network prefix), the full address will be learnt from the 1st uplink
|
||
packet
|
||
- ran_ip is a list with the local IP address and RAN IP address for connecting
|
||
the GTP-U UDP socket endpoints
|
||
-> ran_ip and teid_dl can be None, and set afterwards by calling:
|
||
.set_mobile_dl(teid_ul, ran_ip, teid_dl)
|
||
-> this enables the forwarding of downlink traffic
|
||
To delete GTP tunnel endpoints, use the method:
|
||
.rem_mobile(teid_ul)
|
||
|
||
When a GTP-U packet arrives on the internal interface,
|
||
the IP payload is transferred to the external Gi interface over an Ethernet header.
|
||
When an Ethernet packet arrives on the external Gi interface,
|
||
the IP payload is transferred to the internal interface over a GTP-U header.
|
||
|
||
A little traffic statistics feature can be used with the class attribute:
|
||
.DPI = True
|
||
Traffic statistics are then placed into the attribute .stats
|
||
It is populated even if GTP-U trafic is not forwarded (see BLACKHOLING)
|
||
|
||
A blackholing feature is integrated to disable the forwarding of GTP-U packet
|
||
to the local LAN (with BLACKHOLE_LAN) and/or the routed WAN (with BLACKHOLE_WAN).
|
||
This is done by setting the .BLACKHOLING class attribute.
|
||
|
||
A whitelist feature (TCP/UDP, port) is also integrated.
|
||
To activate if, set the class attribute:
|
||
WL_ACTIVE = True
|
||
Then, only packets for the given protocols / ports are transferred to the Gi,
|
||
by looking into the class attribute:
|
||
WL_PORTS = [('UDP', 53), ('UDP', 123), ('TCP', 80), ...]
|
||
This is bypassing the blackholing feature.
|
||
'''
|
||
#
|
||
# verbosity level: list of log types to display when calling
|
||
# self._log(logtype, msg)
|
||
DEBUG = ('ERR', 'WNG', 'INF', 'DBG')
|
||
#
|
||
# packet buffer space (over MTU...)
|
||
BUFLEN = 2048
|
||
# select loop settings
|
||
SELECT_TO = 0.1
|
||
#
|
||
# Gi interface, with GGSN ethernet IF, MAC address and IPv6 /64 network prefix
|
||
EXT_IF = ARPd.GGSN_ETH_IF
|
||
EXT_MAC_ADDR = ARPd.GGSN_MAC_ADDR
|
||
EXT_IPV6_PREF = '2001:123:456:abcd'
|
||
#
|
||
# list of internal IP interfaces, for handling GTP-U packets from RNCs / eNBs
|
||
GTP_PORT = 2152
|
||
GTP_IF = ('10.1.1.1', '10.2.1.1', )
|
||
#
|
||
# BLACKHOLING feature
|
||
# to enable the whole traffic: 0
|
||
# to disable traffic routed to the WAN: BLACKHOLE_WAN
|
||
# to disable traffic to the local LAN: BLACKHOLE_LAN
|
||
# to disable the whole forwarding of GTP-U packets: BLACKHOLE_LAN | BLACKHOLE_WAN
|
||
BLACKHOLING = 0
|
||
# traffic that we want to allow, even if BLACKHOLING is activated
|
||
WL_ACTIVE = False
|
||
WL_PORTS = [('UDP', 53), ('UDP', 123)]
|
||
#
|
||
# in case we want to generate traffic statistics (then available in .stats)
|
||
DPI = True
|
||
#
|
||
# in case we want to check and drop spoofed IPv4/v6 source address
|
||
# in incoming GTP-U packet
|
||
DROP_SPOOF = True
|
||
#
|
||
# in case we want to stop the listener when typing CTRL+C
|
||
CATCH_SIGINT = False
|
||
|
||
def __init__(self):
|
||
#
|
||
self.EXT_MAC_BUF = mac_aton(self.EXT_MAC_ADDR)
|
||
self.IPV6_NET_PREF = inet_pton(AF_INET6, self.EXT_IPV6_PREF + '::')[:8]
|
||
#
|
||
# 2 dict for handling mobile GTP-U packets transfers:
|
||
# key: mobile IPv4 address or v6 if suffix address (4 or 8 bytes)
|
||
# value: teid_ul (uint)
|
||
self._mobiles_addr = {}
|
||
# key: teid_ul (uint)
|
||
# value: [ran_info (3-tuple: local IP, remote IP, sk_int ref),
|
||
# teid_dl (uint),
|
||
# ipv4_addr (4-bytes or None),
|
||
# ipv6_addr (8-bytes -if addr suffix- or None),
|
||
# ctx_num (uint)]
|
||
self._mobiles_teid = {}
|
||
#
|
||
# initialize the traffic statistics
|
||
self.stats = {}
|
||
self._prot_dict = {1:'ICMP', 6:'TCP', 17:'UDP'}
|
||
# initialize the list of modules that can act on GTP-U payloads
|
||
self.MOD = []
|
||
#
|
||
# create two RAW PF_PACKET sockets on the `Internet` side (1 for IPv4, 1 for IPv6)
|
||
self.sk_ext_v4 = socket.socket(socket.AF_PACKET, socket.SOCK_RAW)
|
||
self.sk_ext_v4.settimeout(0.001)
|
||
#self.sk_ext_v4.setblocking(0)
|
||
self.sk_ext_v4.bind((self.EXT_IF, 0x0800))
|
||
set_promisc(self.sk_ext_v4, self.EXT_IF, 1)
|
||
#
|
||
self.sk_ext_v6 = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, ntohs(0x86dd))
|
||
self.sk_ext_v6.settimeout(0.001)
|
||
#self.sk_ext_v6.setblocking(0)
|
||
self.sk_ext_v6.bind((self.EXT_IF, 0x86dd))
|
||
set_promisc(self.sk_ext_v6, self.EXT_IF, 1)
|
||
#
|
||
# create an UDP socket on the RNC / eNB side
|
||
sk_int, sk_int_ind, ind = [], {}, 0
|
||
for gtpip in self.GTP_IF:
|
||
sk = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||
sk.settimeout(0.001)
|
||
#sk.setblocking(0)
|
||
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||
sk.bind((gtpip, self.GTP_PORT))
|
||
sk_int.append(sk)
|
||
sk_int_ind[gtpip] = ind
|
||
ind += 1
|
||
self.sk_int = tuple(sk_int)
|
||
self._sk_int_ind = sk_int_ind
|
||
#
|
||
# interrupt handler
|
||
if self.CATCH_SIGINT:
|
||
def sigint_handler(signum, frame):
|
||
if self.DEBUG > 1:
|
||
self._log('INF', 'CTRL+C caught')
|
||
self.stop()
|
||
signal.signal(signal.SIGINT, sigint_handler)
|
||
#
|
||
# and start listening and transferring packets in background
|
||
self.sk_list = (self.sk_ext_v4, self.sk_ext_v6) + self.sk_int
|
||
self._listening = True
|
||
self._listener_t = threadit(self.listen)
|
||
self._log('INF', 'GTP-U tunnels handler started')
|
||
#
|
||
# and finally start ARP resolver
|
||
self.arpd = ARPd()
|
||
|
||
def _log(self, logtype='DBG', msg=''):
|
||
# logtype: 'ERR', 'WNG', 'INF', 'DBG'
|
||
if logtype in self.DEBUG:
|
||
log('[%s] [GTPUd] %s' % (logtype, msg))
|
||
|
||
def init_stats(self, ip):
|
||
stats = {
|
||
'DNS' : set(), # IP of DNS servers requested
|
||
'NTP' : set(), # IP of NTP servers requested
|
||
'resolved': set(), # domain name resolved
|
||
'ICMP' : set(), # ICMP endpoint (IP) contacted
|
||
'TCP' : set(), # TCP endpoint (IP, port) contacted
|
||
'UDP' : set(), # UDP endpoint (IP, port) contacted
|
||
'alien' : set(), # other protocol packets
|
||
}
|
||
self.stats[ip] = stats
|
||
return stats
|
||
|
||
def stop(self):
|
||
# stop ARP resolver
|
||
self.arpd.stop()
|
||
# stop local GTPU handler
|
||
if self._listening:
|
||
self._listening = False
|
||
sleep(self.SELECT_TO * 2)
|
||
try:
|
||
set_promisc(self.sk_ext_v4, self.EXT_IF, 0)
|
||
set_promisc(self.sk_ext_v6, self.EXT_IF, 0)
|
||
# closing sockets
|
||
self.sk_ext_v4.close()
|
||
self.sk_ext_v6.close()
|
||
for sk in self.sk_int:
|
||
sk.close()
|
||
except Exception as err:
|
||
self._log('ERR', 'socket error: %s' % err)
|
||
|
||
def listen(self):
|
||
# select() until we receive something on 1 side
|
||
while self._listening:
|
||
r = select(self.sk_list, [], [], self.SELECT_TO)[0]
|
||
# read ext and int sockets until they are empty
|
||
for sk in r:
|
||
#
|
||
if sk == self.sk_ext_v4:
|
||
# DL IPv4
|
||
try:
|
||
buf = sk.recvfrom(self.BUFLEN)[0]
|
||
#except timeout:
|
||
# pass
|
||
except Exception as err:
|
||
self._log('ERR', 'sk_ext_v4 IF error (recvfrom): %s' % err)
|
||
else:
|
||
#self._log('DBG', 'sk_ext_v4, recvfrom()')
|
||
if len(buf) >= 34 and buf[:6] == self.EXT_MAC_BUF \
|
||
and buf[30:34] in self._mobiles_addr:
|
||
# IPv4 of a mobile, transfer over GTP-U
|
||
# after removing the Ethernet header
|
||
self.transfer_v4_to_int(buf[14:])
|
||
#threadit(self.transfer_v4_to_int, buf[14:])
|
||
#
|
||
elif sk == self.sk_ext_v6:
|
||
# DL IPv6
|
||
try:
|
||
buf = sk.recvfrom(self.BUFLEN)[0]
|
||
#except timeout:
|
||
# pass
|
||
except Exception as err:
|
||
self._log('ERR', 'sk_ext_v6 IF error (recvfrom): %s' % err)
|
||
else:
|
||
#self._log('DBG', 'sk_ext_v6, recvfrom()')
|
||
if len(buf) >= 54 and buf[:6] == self.EXT_MAC_BUF \
|
||
and buf[46:54] in self._mobiles_addr:
|
||
# IPv6 of a mobile, transfer over GTP-U
|
||
# after removing the Ethernet header
|
||
self.transfer_v6_to_int(buf[14:])
|
||
#threadit(self.transfer_v6_to_int, buf[14:])
|
||
#
|
||
else:
|
||
#sk in self.sk_int
|
||
# UL, both IPv4 and IPv6 packets
|
||
try:
|
||
buf = sk.recv(self.BUFLEN)
|
||
#except timeout:
|
||
# pass
|
||
except Exception as err:
|
||
self._log('ERR', 'sk_int IF error (recv): %s' % err)
|
||
else:
|
||
self.transfer_to_ext(buf)
|
||
#threadit(self.transfer_to_ext, buf)
|
||
#
|
||
self._log('INF', 'GTPU handler stopped')
|
||
|
||
def resolve_mac(self, ipdst):
|
||
if len(ipdst) == 4:
|
||
return self.arpd.resolve(inet_ntoa(ipdst))
|
||
else:
|
||
# TODO: implement a minimal IPv6 NDP service ?
|
||
return self.arpd.ROUTER_MAC_BUF
|
||
|
||
#--------------------------------------------------------------------------#
|
||
# UL transfer
|
||
#--------------------------------------------------------------------------#
|
||
|
||
def transfer_to_ext(self, buf):
|
||
try:
|
||
# extract the GTP header
|
||
flags, msgtype, msglen, teid_ul = unpack('>BBHI', buf[:8])
|
||
ran_info, teid_dl, ipv4buf, ipv6buf, ctx_num = self._mobiles_teid[teid_ul]
|
||
if msgtype != 0xff:
|
||
# TODO: handle GTP ECHO
|
||
self._log('WNG', 'unsupported GTP type from RAN: 0x%.2x' % msgtype)
|
||
return
|
||
# get the IP packet: use the length in the GTP header to cut the buffer
|
||
if flags & 0b111:
|
||
# GTP header extended
|
||
msglen -= 4
|
||
ipbuf = buf[-msglen:]
|
||
# get the IP version
|
||
ipvers = ord(ipbuf[0:1])>>4
|
||
if ipvers == 4:
|
||
ipsrc = ipbuf[12:16]
|
||
ipdst = ipbuf[16:20]
|
||
elif ipvers == 6:
|
||
ipsrc = ipbuf[8:24]
|
||
ipdst = ipbuf[24:40]
|
||
else:
|
||
self._log('WNG', 'invalid IP packet from UE, dropping it')
|
||
return
|
||
except Exception:
|
||
self._log('WNG', 'invalid GTP / IP packet from RAN / UE, dropping it')
|
||
return
|
||
#
|
||
if ipvers == 4:
|
||
if self.DROP_SPOOF and ipsrc != ipv4buf:
|
||
self._log('WNG', 'spoofed IPv4 src addr, teid_ul 0x%.8x' % teid_ul)
|
||
return
|
||
if self.DPI:
|
||
self._analyze(ipvers, inet_ntoa(ipsrc), ipbuf)
|
||
if self.MOD:
|
||
try:
|
||
for mod in self.MOD:
|
||
if mod.TYPE == 0:
|
||
ipbuf = mod.handle_ul(ipbuf)
|
||
else:
|
||
mod.handle_ul(ipbuf)
|
||
except Exception as err:
|
||
self._log('ERR', 'MOD error: %s' % err)
|
||
# resolve the dest MAC addr
|
||
macdst = self.resolve_mac(ipdst)
|
||
# apply blackholing
|
||
if self.BLACKHOLING:
|
||
if macdst != self.arpd.ROUTER_MAC_BUF:
|
||
if self.BLACKHOLING & BLACKHOLE_LAN:
|
||
drop = True
|
||
else:
|
||
drop = False
|
||
else:
|
||
if self.BLACKHOLING & BLACKHOLE_WAN:
|
||
drop = True
|
||
else:
|
||
drop = False
|
||
if drop and self.WL_ACTIVE:
|
||
ipdst, prot, pay = DPIv4.get_ip_info(ipbuf)
|
||
if prot in (6, 17) and pay:
|
||
# UDP / TCP
|
||
port = DPIv4.get_port(pay)
|
||
if (self._prot_dict[prot], port) in self.WL_PORTS:
|
||
self._transfer_v4_to_ext(macdst, ipbuf)
|
||
else:
|
||
return
|
||
else:
|
||
self._transfer_v4_to_ext(macdst, ipbuf)
|
||
#
|
||
else:
|
||
#ipvers == 6
|
||
if self.DROP_SPOOF and ipsrc[8:] != ipv6buf:
|
||
self._log('WNG', 'spoofed IPv6 src addr, teid_ul 0x%.8x' % teid_ul)
|
||
return
|
||
if self.DPI:
|
||
self._analyze(ipvers, inet_ntop(AF_INET6, ipsrc), ipbuf)
|
||
if self.MOD:
|
||
try:
|
||
for mod in self.MOD:
|
||
if mod.TYPE == 0:
|
||
ipbuf = mod.handle_ul(ipbuf)
|
||
else:
|
||
mod.handle_ul(ipbuf)
|
||
except Exception as err:
|
||
self._log('ERR', 'MOD error: %s' % err)
|
||
# resolve the dest MAC addr
|
||
macdst = self.resolve_mac(ipdst)
|
||
# apply blackholing
|
||
if self.BLACKHOLING:
|
||
if macdst != self.arpd.ROUTER_MAC_BUF:
|
||
if self.BLACKHOLING & BLACKHOLE_LAN:
|
||
drop = True
|
||
else:
|
||
drop = False
|
||
else:
|
||
if self.BLACKHOLING & BLACKHOLE_WAN:
|
||
drop = True
|
||
else:
|
||
drop = False
|
||
if drop and self.WL_ACTIVE:
|
||
ipdst, prot, pay = DPIv6.get_ip_info(ipbuf)
|
||
if prot in (6, 17) and pay:
|
||
# UDP / TCP
|
||
port = DPIv6.get_port(pay)
|
||
if (self._prot_dict[prot], port) in self.WL_PORTS:
|
||
self._transfer_v6_to_ext(macdst, ipbuf)
|
||
else:
|
||
return
|
||
else:
|
||
self._transfer_v6_to_ext_v6(macdst, ipbuf)
|
||
|
||
def _transfer_v4_to_ext(self, macdst, ipbuf):
|
||
# forward to the external PF_PACKET socket, over the Gi interface
|
||
try:
|
||
self.sk_ext_v4.sendto(b''.join((macdst, self.EXT_MAC_BUF, b'\x08\0', ipbuf)),
|
||
(self.EXT_IF, 0x0800))
|
||
except Exception as err:
|
||
self._log('ERR', 'sk_ext_v4 IF error (sendto): %s' % err)
|
||
|
||
def _transfer_v6_to_ext(self, macdst, ipbuf):
|
||
# forward to the external PF_PACKET socket, over the Gi interface
|
||
try:
|
||
self.sk_ext_v6.sendto(b''.join((macdst, self.EXT_MAC_BUF, b'\x86\xdd', ipbuf)),
|
||
(self.EXT_IF, 0x86dd))
|
||
except Exception as err:
|
||
self._log('ERR', 'sk_ext_v6 IF error (sendto): %s' % err)
|
||
|
||
def _analyze(self, ipvers, ipsrc, ipbuf):
|
||
#
|
||
try:
|
||
stats = self.stats[ipsrc]
|
||
except Exception:
|
||
stats = self.init_stats(ipsrc)
|
||
#
|
||
if ipvers == 4:
|
||
dst, prot, pay = DPIv4.get_ip_info(ipbuf)
|
||
DPI = DPIv4
|
||
else:
|
||
dst, prot, pay = DPIv6.get_ip_info(ipbuf)
|
||
DPI = DPIv6
|
||
#
|
||
# UDP
|
||
if prot == 17 and pay:
|
||
port = DPI.get_port(pay)
|
||
stats['UDP'].add((dst, port))
|
||
# DNS
|
||
if port == 53:
|
||
stats['DNS'].add(dst)
|
||
name = DPI.get_dn_req(pay[8:])
|
||
stats['resolved'].add(name)
|
||
elif port == 123:
|
||
stats['NTP'].add(dst)
|
||
# TCP
|
||
elif prot == 6 and pay:
|
||
port = DPI.get_port(pay)
|
||
stats['TCP'].add((dst, port))
|
||
# ICMP / ICMPv6
|
||
elif prot in (1, 58) and pay:
|
||
stats['ICMP'].add(dst)
|
||
# alien
|
||
else:
|
||
stats['alien'].add(hexlify(ipbuf))
|
||
|
||
#--------------------------------------------------------------------------#
|
||
# DL transfer
|
||
#--------------------------------------------------------------------------#
|
||
|
||
def transfer_v4_to_int(self, buf):
|
||
#self._log('DBG', 'transfer_v4_to_int()')
|
||
# buf length is guaranteed >= 20 and ipdst in self._mobiles_addr
|
||
#
|
||
if self.MOD:
|
||
# possibly process the DL GTP-U payload within modules
|
||
try:
|
||
for mod in self.MOD:
|
||
if mod.TYPE == 0:
|
||
buf = mod.handle_dl(buf)
|
||
else:
|
||
mod.handle_dl(buf)
|
||
except Exception as err:
|
||
self._log('ERR', 'MOD error: %s' % err)
|
||
#
|
||
teid_ul = self._mobiles_addr[buf[16:20]]
|
||
ran_info, teid_dl = self._mobiles_teid[teid_ul][:2]
|
||
#
|
||
# prepend GTP header and forward to the RAN IP
|
||
if ran_info and teid_dl is not None:
|
||
gtphdr = pack('>BBHI', 0x30, 0xff, len(buf), teid_dl)
|
||
try:
|
||
ret = ran_info[2].sendto(gtphdr + buf, (ran_info[1], self.GTP_PORT))
|
||
except Exception as err:
|
||
self._log('ERR', 'sk_int IF error (sendto): %s' % err)
|
||
else:
|
||
self._log('WNG', 'teid_ul 0x%.8x, downlink GTP parameters not set' % teid_ul)
|
||
|
||
def transfer_v6_to_int(self, buf):
|
||
#self._log('DBG', 'transfer_v6_to_int()')
|
||
# buf length is guaranteed >= 40 and ipdst in self._mobiles_addr
|
||
#
|
||
if self.MOD:
|
||
# possibly process the DL GTP-U payload within modules
|
||
try:
|
||
for mod in self.MOD:
|
||
if mod.TYPE == 0:
|
||
buf = mod.handle_dl(buf)
|
||
else:
|
||
mod.handle_dl(buf)
|
||
except Exception as err:
|
||
self._log('ERR', 'MOD error: %s' % err)
|
||
#
|
||
teid_ul = self._mobiles_addr[buf[32:40]]
|
||
ran_info, teid_dl = self._mobiles_teid[teid_ul][:2]
|
||
#
|
||
# prepend GTP header and forward to the RAN IP
|
||
if ran_info and teid_dl is not None:
|
||
gtphdr = pack('>BBHI', 0x30, 0xff, len(buf), teid_dl)
|
||
try:
|
||
ret = ran_info[2].sendto(gtphdr + buf, (ran_info[1], self.GTP_PORT))
|
||
except Exception as err:
|
||
self._log('ERR', 'sk_int IF error (sendto): %s' % err)
|
||
else:
|
||
self._log('WNG', 'teid_ul 0x%.8x, downlink GTP parameters not set' % teid_ul)
|
||
|
||
#--------------------------------------------------------------------------#
|
||
# UE management
|
||
#--------------------------------------------------------------------------#
|
||
|
||
def add_mobile(self, teid_ul, mobile_addr, ran_ip, teid_dl):
|
||
if teid_ul in self._mobiles_teid:
|
||
# just increment the ctx_num
|
||
self._mobiles_teid[teid_ul][-1] += 1
|
||
#
|
||
else:
|
||
if mobile_addr[0] == 1:
|
||
# IPv4
|
||
ipv4buf = inet_aton_cn(*mobile_addr)
|
||
if len(ipv4buf) != 4:
|
||
self._log('ERR', 'invalid mobile addr %r' % (mobile_addr, ))
|
||
return
|
||
ipv6buf = None
|
||
elif mobile_addr[0] == 2:
|
||
# IPv6 if suffix (8 bytes) or full IPv6 (then truncated to 8 bytes)
|
||
ipv6buf = inet_aton_cn(*mobile_addr)
|
||
if len(ipv6buf) == 16:
|
||
ipv6buf = ipv6buf[8:]
|
||
elif len(ipv6buf) != 8:
|
||
self._log('ERR', 'invalid mobile addr %r' % (mobile_addr, ))
|
||
return
|
||
ipv4buf = None
|
||
elif mobile_addr[0] == 3:
|
||
# IPv4v6
|
||
# IPv4
|
||
ipv4buf = inet_aton_cn(1, mobile_addr[1])
|
||
if len(ipv4buf) != 4:
|
||
self._log('ERR', 'invalid mobile addr %r' % (mobile_addr, ))
|
||
return
|
||
# IPv6 if suffix (8 bytes) or full IPv6
|
||
ipv6buf = inet_aton_cn(2, mobile_addr[2])
|
||
if len(ipv6buf) == 16:
|
||
ipv6buf = ipv6buf[8:]
|
||
elif len(ipv6buf) != 8:
|
||
self._log('ERR', 'invalid mobile addr %r' % (mobile_addr, ))
|
||
return
|
||
else:
|
||
self._log('ERR', 'invalid mobile addr %r' % (mobile_addr, ))
|
||
#
|
||
if ran_ip and ran_ip[1] is not None:
|
||
try:
|
||
# add the sk_int within ran_info
|
||
sk_int = self.sk_int[self._sk_int_ind[ran_ip[0]]]
|
||
ran_info = (ran_ip[0], ran_ip[1], sk_int)
|
||
except Exception:
|
||
self._log('ERR', 'invalid RAN IP, %r' % ran_ip)
|
||
ran_info = None
|
||
else:
|
||
ran_info = None
|
||
# insert a new context
|
||
self._mobiles_teid[teid_ul] = [ran_info, teid_dl, ipv4buf, ipv6buf, 1]
|
||
if ipv4buf:
|
||
self._mobiles_addr[ipv4buf] = teid_ul
|
||
if ipv6buf:
|
||
self._mobiles_addr[ipv6buf] = teid_ul
|
||
#
|
||
self._log('INF', 'setting GTP-U context for UE with IP %r, teid_ul 0x%.8x'\
|
||
% (mobile_addr, teid_ul))
|
||
|
||
def set_mobile_dl(self, teid_ul, ran_ip=None, teid_dl=None):
|
||
# enables to reconfigure the DL parameters (RAN IP, DL TEID)
|
||
try:
|
||
ran_info_ori, teid_dl_ori, ipv4buf, ipv6buf, ctx_num = self._mobiles_teid[teid_ul]
|
||
except Exception as err:
|
||
self._log('ERR', 'invalid teid_ul 0x%.8x' % teid_ul)
|
||
return
|
||
else:
|
||
if ran_ip:
|
||
try:
|
||
# add the sk_int within ran_info
|
||
sk_int = self.sk_int[self._sk_int_ind[ran_ip[0]]]
|
||
ran_info = (ran_ip[0], ran_ip[1], sk_int)
|
||
except Exception:
|
||
self._log('ERR', 'invalid RAN IP, %r' % ran_ip)
|
||
ran_info = None
|
||
else:
|
||
ran_info = None
|
||
if teid_dl is None:
|
||
teid_dl = teid_dl_ori
|
||
self._mobiles_teid[teid_ul] = [ran_info, teid_dl, ipv4buf, ipv6buf, ctx_num]
|
||
|
||
def rem_mobile(self, teid_ul):
|
||
if teid_ul in self._mobiles_teid:
|
||
mobile_ctx = self._mobiles_teid[teid_ul]
|
||
if mobile_ctx[-1] > 1:
|
||
# decrement the number of GTP contexts
|
||
mobile_ctx[-1] -= 1
|
||
else:
|
||
# delete the mobile context
|
||
del self._mobiles_teid[teid_ul]
|
||
ran_info, teid_dl, ipv4buf, ipv6buf, ctx_num = mobile_ctx
|
||
if ipv4buf:
|
||
ipv4addr = inet_ntoa(ipv4buf)
|
||
try:
|
||
del self._mobiles_addr[ipv4buf]
|
||
except Exception:
|
||
pass
|
||
else:
|
||
ipv4addr = None
|
||
if ipv6buf:
|
||
ipv6addr = inet_ntop(AF_INET6, self.IPV6_NET_PREF + ipv6buf)
|
||
try:
|
||
del self._mobiles_addr[ipv6buf]
|
||
except Exception:
|
||
pass
|
||
else:
|
||
ipv6addr = None
|
||
if ipv4addr and ipv6addr:
|
||
ipaddr = 'IPv4 %s / IPv6 %s' % (ipv4addr, ipv6addr)
|
||
elif ipv6addr is None:
|
||
ipaddr = 'IPv4 ' + ipv4addr
|
||
else:
|
||
ipaddr = 'IPv6 ' + ipv6addr
|
||
self._log('DBG', 'deleting GTP-U context for UE with addr %s, teid_ul 0x%.8x'\
|
||
% (ipaddr, teid_ul))
|
||
|
||
|
||
class _DPI(object):
|
||
|
||
@staticmethod
|
||
def get_port(pay):
|
||
"""return the port TCP / UDP number
|
||
"""
|
||
return unpack('!H', pay[2:4])[0]
|
||
|
||
@staticmethod
|
||
def __get_dn_req_py2(req):
|
||
"""return the DNS name requested
|
||
"""
|
||
# remove fixed DNS header and Type / Class
|
||
s = req[12:-4]
|
||
n = []
|
||
while len(s) > 1:
|
||
l = ord(s[0])
|
||
n.append( s[1:1+l] )
|
||
s = s[1+l:]
|
||
return b'.'.join(n)
|
||
|
||
@staticmethod
|
||
def __get_dn_req_py3(req):
|
||
"""return the DNS name requested
|
||
"""
|
||
# remove fixed DNS header and Type / Class
|
||
s = req[12:-4]
|
||
n = []
|
||
while len(s) > 1:
|
||
l = s[0]
|
||
n.append( s[1:1+l] )
|
||
s = s[1+l:]
|
||
return b'.'.join(n)
|
||
|
||
if python_version < 3:
|
||
get_dn_req = __get_dn_req_py2
|
||
else:
|
||
get_dn_req = __get_dn_req_py3
|
||
|
||
|
||
class DPIv4(_DPI):
|
||
|
||
@staticmethod
|
||
def __get_ip_info_py2(ipbuf):
|
||
"""return a 3-tuple: ipdst (asc), protocol (uint), payload (bytes)
|
||
"""
|
||
# returns a 3-tuple: dst IP, protocol, payload buffer
|
||
# get IP header length
|
||
l = (ord(ipbuf[0]) & 0x0F) * 4
|
||
# get dst IP
|
||
dst = inet_ntoa(ipbuf[16:20])
|
||
# get protocol
|
||
prot = ord(ipbuf[9])
|
||
#
|
||
return (dst, prot, ipbuf[l:])
|
||
|
||
@staticmethod
|
||
def __get_ip_info_py3(ipbuf):
|
||
"""return a 3-tuple: ipdst (asc), protocol (uint), payload (bytes)
|
||
"""
|
||
# returns a 3-tuple: dst IP, protocol, payload buffer
|
||
# get IP header length
|
||
l = (ipbuf[0] & 0x0F) * 4
|
||
# get dst IP
|
||
dst = inet_ntoa(ipbuf[16:20])
|
||
# get protocol
|
||
prot = ipbuf[9]
|
||
#
|
||
return (dst, prot, ipbuf[l:])
|
||
|
||
if python_version < 3:
|
||
get_ip_info = __get_ip_info_py2
|
||
else:
|
||
get_ip_info = __get_ip_info_py3
|
||
|
||
|
||
class DPIv6(_DPI):
|
||
|
||
@staticmethod
|
||
def __get_ip_info_py2(ipbuf):
|
||
"""return a 3-tuple: ipdst (asc), protocol (uint), payload (bytes)
|
||
"""
|
||
# returns a 3-tuple: dst IP, protocol, payload buffer
|
||
# get payload length
|
||
pl = unpack('>H', ipbuf[4:6])[0]
|
||
# get dst IP
|
||
dst = inet_ntop(AF_INET6, ipbuf[24:40])
|
||
# get protocol
|
||
# TODO: unstack IPv6 opts
|
||
prot = ord(ipbuf[6])
|
||
#
|
||
return (dst, prot, ipbuf[-pl:])
|
||
|
||
@staticmethod
|
||
def __get_ip_info_py3(ipbuf):
|
||
"""return a 3-tuple: ipdst (asc), protocol (uint), payload (bytes)
|
||
"""
|
||
# returns a 3-tuple: dst IP, protocol, payload buffer
|
||
# get payload length
|
||
pl = unpack('>H', ipbuf[4:6])[0]
|
||
# get dst IP
|
||
dst = inet_ntop(AF_INET6, ipbuf[24:40])
|
||
# get protocol
|
||
# TODO: unstack IPv6 opts
|
||
prot = ipbuf[6]
|
||
#
|
||
return (dst, prot, ipbuf[-pl:])
|
||
|
||
if python_version < 3:
|
||
get_ip_info = __get_ip_info_py2
|
||
else:
|
||
get_ip_info = __get_ip_info_py3
|
||
|
||
|
||
class MOD(object):
|
||
# This is a skeleton for GTP-U payloads specific handler.
|
||
# After It gets loaded by the GTPUd instance,
|
||
# it acts on each GTP-U payloads (UL and DL)
|
||
|
||
# In can work actively on GTP-U packets (possibly changing them)
|
||
# with TYPE = 0
|
||
# or passively (not able to change them), only processing a copy of them,
|
||
# with TYPE = 1
|
||
TYPE = 0
|
||
|
||
# reference to the GTPUd instance
|
||
GTPUd = None
|
||
|
||
@classmethod
|
||
def _log(self, logtype, msg):
|
||
self.GTPUd._log(logtype, '[MOD.%s] %s' % (self.__class__.__name__, msg))
|
||
|
||
@classmethod
|
||
def handle_ul(self, ippuf):
|
||
pass
|
||
|
||
@classmethod
|
||
def handle_dl(self, ipbuf):
|
||
pass
|
||
|
||
|
||
class DNSRESP(MOD):
|
||
'''
|
||
This module answers to any DNS request incoming from UE (UL direction)
|
||
with a single or random IP address, over IPv4
|
||
|
||
To be used with GTPUd.BLACKHOLING capability to avoid UE getting real
|
||
DNS responses from servers in parallel
|
||
'''
|
||
TYPE = 1
|
||
|
||
# compute UDP checksum in DNS response
|
||
UDP_CS = True
|
||
# in case we want to answer random addresses
|
||
RAND = False
|
||
# the IPv4 address to answer all requests
|
||
IP_RESP = '192.168.1.50'
|
||
|
||
DEBUG = False
|
||
|
||
@classmethod
|
||
def handle_ul(self, ipbuf):
|
||
# check if we have an UDP/53 request
|
||
ip_vers, ip_proto, (udpsrc, udpdst) = \
|
||
ord(ipbuf[0:1])>>4, ord(ipbuf[9:10]), unpack('!HH', ipbuf[20:24])
|
||
if ip_vers != 4 or ip_proto != 53 or udp_dst != 53:
|
||
# not IPv4, not UDP or not on DNS port 53
|
||
return
|
||
|
||
# build the UDP / DNS response: invert src / dst UDP ports
|
||
if self.UDP_CS:
|
||
udp = UDP(val={'src':udpdst, 'dst':udpsrc}, hier=1)
|
||
else:
|
||
udp = UDP(val={'src':udpdst, 'dst':udpsrc, 'cs':0}, hier=1)
|
||
# DNS request: transaction id, flags, questions, queries
|
||
dnsreq = ipbuf[28:]
|
||
transac_id, questions, queries = dnsreq[0:2], \
|
||
unpack('!H', dnsreq[4:6])[0], \
|
||
dnsreq[12:]
|
||
if questions > 1:
|
||
# not supported
|
||
self._log('WNG', '%i questions, unsupported' % questions)
|
||
# DNS response: transaction id, flags, questions, answer RRs,
|
||
# author RRs, add RRs, queries, answers, autor nameservers, add records
|
||
if self.RAND:
|
||
ip_resp = _urandom(4)
|
||
else:
|
||
ip_resp = inet_aton(self.IP_RESP)
|
||
dnsresp = b''.join((transac_id, b'\x81\x80\0\x01\0\x01\0\0\0\0', queries,
|
||
b'\xc0\x0c\0\x01\0\x01\0\0\0\x20\0\x04', ip_resp))
|
||
|
||
# build the IPv4 header: invert src / dst addr
|
||
ipsrc, ipdst = inet_ntoa(ipbuf[12:16]), inet_ntoa(ipbuf[16:20])
|
||
iphdr = IPv4(val={'src':ipdst, 'dst':ipsrc}, hier=0)
|
||
#
|
||
pkt = Envelope('p', GEN=(iphdr, udp, Buf('dns', val=dnsresp, hier=2)))
|
||
# send back the DNS response
|
||
self.GTPUd.transfer_v4_to_int(pkt.to_bytes())
|
||
if self.DEBUG:
|
||
self.GTPUd._log('DBG', '[DNSRESP] DNS response sent')
|
||
|
||
|
||
class TCPSYNACK(MOD):
|
||
'''
|
||
This module answers to TCP SYN request incoming from UE (UL direction)
|
||
over IPv4 with a TCP SYN-ACK, enabling to get the 1st TCP data packet
|
||
from the UE
|
||
|
||
To be used with GTPUd.BLACKHOLING capability to avoid UE getting SYN-ACK
|
||
from real servers in parallel
|
||
'''
|
||
TYPE = 1
|
||
|
||
DEBUG = False
|
||
|
||
@classmethod
|
||
def handle_ul(self, ipbuf):
|
||
# check if we have a TCP SYN
|
||
ip_vers, ip_proto, ip_pay = ord(ipbuf[0:1])>>4, ord(ipbuf[9:10]), ipbuf[20:]
|
||
if ip_vers != 4 or ip_proto != 6 or ip_pay[13:14] != b'\x02':
|
||
# not IPv4, not TCP, not SYN
|
||
return
|
||
|
||
# build the TCP SYN-ACK: invert src / dst ports, seq num (random),
|
||
# ack num (SYN seq num + 1)
|
||
tcpsrc, tcpdst, seq = unpack('!HHI', ip_pay[:8])
|
||
tcp_synack = TCP(val={'seq': randint(1, 4294967295),
|
||
'ack': (1+seq)%4294967296,
|
||
'src': tcpdst, 'dst': tcpsrc,
|
||
'SYN': 1, 'ACK': 1, 'win': 0x1000}, hier=1)
|
||
|
||
# build the IPv4 header: invert src / dst addr
|
||
ipsrc, ipdst = inet_ntoa(ipbuf[12:16]), inet_ntoa(ipbuf[16:20])
|
||
iphdr = IPv4(val={'src':ipdst, 'dst':ipsrc}, hier=0)
|
||
#
|
||
pkt = Envelope('p', GEN=(iphdr, tcp_synack))
|
||
# send back the TCP SYN-ACK
|
||
self.GTPUd.transfer_v4_to_int(pkt.to_bytes())
|
||
if self.DEBUG:
|
||
self.GTPUd._log('DBG', '[TCPSYNACK] TCP SYN ACK response sent')
|
||
|