aboutsummaryrefslogtreecommitdiffstats
path: root/node-admin/scripts/pyroute2/dhcp/dhcp4socket.py
blob: 7928d5a074567297f8cf01cad93019c96817f230 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# By Peter V. Saveliev https://pypi.python.org/pypi/pyroute2. Dual licensed under the Apache 2 and GPLv2+ see https://github.com/svinota/pyroute2 for License details.
'''
IPv4 DHCP socket
================

'''
from pyroute2.common import AddrPool
from pyroute2.protocols import udpmsg
from pyroute2.protocols import udp4_pseudo_header
from pyroute2.protocols import ethmsg
from pyroute2.protocols import ip4msg
from pyroute2.protocols.rawsocket import RawSocket
from pyroute2.dhcp.dhcp4msg import dhcp4msg


def listen_udp_port(port=68):
    # pre-scripted BPF code that matches UDP port
    bpf_code = [[40, 0, 0, 12],
                [21, 0, 8, 2048],
                [48, 0, 0, 23],
                [21, 0, 6, 17],
                [40, 0, 0, 20],
                [69, 4, 0, 8191],
                [177, 0, 0, 14],
                [72, 0, 0, 16],
                [21, 0, 1, port],
                [6, 0, 0, 65535],
                [6, 0, 0, 0]]
    return bpf_code


class DHCP4Socket(RawSocket):
    '''
    Parameters:

    * ifname -- interface name to work on

    This raw socket binds to an interface and installs BPF filter
    to get only its UDP port. It can be used in poll/select and
    provides also the context manager protocol, so can be used in
    `with` statements.

    It does not provide any DHCP state machine, and does not inspect
    DHCP packets, it is totally up to you. No default values are
    provided here, except `xid` -- DHCP transaction ID. If `xid` is
    not provided, DHCP4Socket generates it for outgoing messages.
    '''

    def __init__(self, ifname, port=68):
        RawSocket.__init__(self, ifname, listen_udp_port(port))
        self.port = port
        # Create xid pool
        #
        # Every allocated xid will be released automatically after 1024
        # alloc() calls, there is no need to call free(). Minimal xid == 16
        self.xid_pool = AddrPool(minaddr=16, release=1024)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.close()

    def put(self, msg=None, dport=67):
        '''
        Put DHCP message. Parameters:

        * msg -- dhcp4msg instance
        * dport -- DHCP server port

        If `msg` is not provided, it is constructed as default
        BOOTREQUEST + DHCPDISCOVER.

        Examples::

            sock.put(dhcp4msg({'op': BOOTREQUEST,
                               'chaddr': 'ff:11:22:33:44:55',
                               'options': {'message_type': DHCPREQUEST,
                                           'parameter_list': [1, 3, 6, 12, 15],
                                           'requested_ip': '172.16.101.2',
                                           'server_id': '172.16.101.1'}}))

        The method returns dhcp4msg that was sent, so one can get from
        there `xid` (transaction id) and other details.
        '''
        # DHCP layer
        dhcp = msg or dhcp4msg({'chaddr': self.l2addr})

        # dhcp transaction id
        if dhcp['xid'] is None:
            dhcp['xid'] = self.xid_pool.alloc()

        data = dhcp.encode().buf

        # UDP layer
        udp = udpmsg({'sport': self.port,
                      'dport': dport,
                      'len': 8 + len(data)})
        udph = udp4_pseudo_header({'dst': '255.255.255.255',
                                   'len': 8 + len(data)})
        udp['csum'] = self.csum(udph.encode().buf + udp.encode().buf + data)
        udp.reset()

        # IPv4 layer
        ip4 = ip4msg({'len': 20 + 8 + len(data),
                      'proto': 17,
                      'dst': '255.255.255.255'})
        ip4['csum'] = self.csum(ip4.encode().buf)
        ip4.reset()

        # MAC layer
        eth = ethmsg({'dst': 'ff:ff:ff:ff:ff:ff',
                      'src': self.l2addr,
                      'type': 0x800})

        data = eth.encode().buf +\
            ip4.encode().buf +\
            udp.encode().buf +\
            data
        self.send(data)
        dhcp.reset()
        return dhcp

    def get(self):
        '''
        Get the next incoming packet from the socket and try
        to decode it as IPv4 DHCP. No analysis is done here,
        only MAC/IPv4/UDP headers are stripped out, and the
        rest is interpreted as DHCP.
        '''
        (data, addr) = self.recvfrom(4096)
        eth = ethmsg(buf=data).decode()
        ip4 = ip4msg(buf=data, offset=eth.offset).decode()
        udp = udpmsg(buf=data, offset=ip4.offset).decode()
        return dhcp4msg(buf=data, offset=udp.offset).decode()