ci: cram: add DHCPv4 Force option cram tests
Test the Force parameter of Device.DHCPv4.Server.Pool.{i}.Options.{i} to
force an option when ever it is not requested by the client
Closes: PCF-2462
Signed-off-by: Alexandre Fiset <alexandre.fiset@softathome.com>
This commit is contained in:
parent
850b5641d1
commit
951c2d1d74
1
.gitignore
vendored
1
.gitignore
vendored
@ -35,3 +35,4 @@ git-src
|
||||
.cproject
|
||||
.ccache
|
||||
.vscode*
|
||||
__pycache__/
|
||||
|
||||
@ -0,0 +1,54 @@
|
||||
Create R alias:
|
||||
|
||||
$ alias R="${CRAM_REMOTE_COMMAND:-}"
|
||||
|
||||
Install scapy with pip:
|
||||
|
||||
$ pip install scapy >/dev/null 2>&1
|
||||
|
||||
Verify python can import scapy:
|
||||
|
||||
$ python3 -c "import scapy.all" >/dev/null 2>&1
|
||||
|
||||
Pick test constants:
|
||||
|
||||
$ TAG=42
|
||||
$ VALUE_HEX=74657374
|
||||
$ REQ_WITHOUT_TAG=1,3,6,15
|
||||
$ REQ_WITH_TAG=1,3,6,15,42
|
||||
$ PROBE_TIMEOUT=12
|
||||
|
||||
Create pool option with Force=true:
|
||||
|
||||
$ R "ba-cli 'Device.DHCPv4.Server.Pool.lan.Option.+{Alias=\"cpe-force-opt\",Enable=1,Tag=${TAG},Value=\"${VALUE_HEX}\",Force=1}'" | grep -Ev '^(>|$)'
|
||||
Device\.DHCPv4\.Server\.Pool\.1\.Option\.[0-9]+\. (re)
|
||||
Device\.DHCPv4\.Server\.Pool\.1\.Option\.[0-9]+\.Alias="cpe-force-opt" (re)
|
||||
$ sleep 1
|
||||
|
||||
Force=true, tag 42 absent from DHCPREQUEST PRL -> tag must be present in final DHCPACK:
|
||||
|
||||
$ sudo -E python3 "$TESTDIR/dhcpv4_option_probe.py" --iface "$TESTBED_LAN_INTERFACE" --request-options "$REQ_WITHOUT_TAG" --timeout "$PROBE_TIMEOUT" 2>/dev/null
|
||||
RECEIVED_TAGS=.*(=|,)42(,|$).* (re)
|
||||
|
||||
Set Force=false:
|
||||
|
||||
$ R "ba-cli 'Device.DHCPv4.Server.Pool.lan.Option.cpe-force-opt.Force=0'" | grep -Ev '^(>|$)'
|
||||
Device\.DHCPv4\.Server\.Pool\.1\.Option\.[0-9]+\. (re)
|
||||
Device\.DHCPv4\.Server\.Pool\.1\.Option\.[0-9]+\.Force=0 (re)
|
||||
$ sleep 1
|
||||
|
||||
Force=false, tag 42 absent from DHCPREQUEST PRL -> tag must be absent from final DHCPACK:
|
||||
|
||||
$ sudo -E python3 "$TESTDIR/dhcpv4_option_probe.py" --iface "$TESTBED_LAN_INTERFACE" --request-options "$REQ_WITHOUT_TAG" --timeout "$PROBE_TIMEOUT" 2>/dev/null
|
||||
RECEIVED_TAGS=(?!.*(=|,)42(,|$)).* (re)
|
||||
|
||||
Force=false, tag 42 present in DHCPREQUEST PRL -> tag must be present in final DHCPACK:
|
||||
|
||||
$ sudo -E python3 "$TESTDIR/dhcpv4_option_probe.py" --iface "$TESTBED_LAN_INTERFACE" --request-options "$REQ_WITH_TAG" --timeout "$PROBE_TIMEOUT" 2>/dev/null
|
||||
RECEIVED_TAGS=.*(=|,)42(,|$).* (re)
|
||||
|
||||
|
||||
Cleanup:
|
||||
|
||||
$ R "ba-cli 'Device.DHCPv4.Server.Pool.lan.Option.cpe-force-opt.-'" | grep -Ev '^(>|$)'
|
||||
Device\.DHCPv4\.Server\.Pool\.1\.Option\.[0-9]+\. (re)
|
||||
@ -0,0 +1,76 @@
|
||||
# DHCPv4 Option Probe Client (Python)
|
||||
|
||||
This directory contains the Python DHCPv4 probe client:
|
||||
|
||||
- `dhcpv4_option_probe.py`
|
||||
|
||||
## What the client checks
|
||||
|
||||
The client performs the DHCP exchange:
|
||||
|
||||
- `DHCPDISCOVER`
|
||||
- `DHCPOFFER`
|
||||
- `DHCPREQUEST`
|
||||
- `DHCPACK`
|
||||
|
||||
Option checks are based on:
|
||||
|
||||
- options requested in `DHCPREQUEST` (`param_req_list`)
|
||||
- options received in the final `DHCPACK`
|
||||
|
||||
The output is extracted from the final `DHCPACK` only.
|
||||
|
||||
## Requirements
|
||||
|
||||
- `python3`
|
||||
- `scapy`
|
||||
- root privileges (raw packet send/receive)
|
||||
|
||||
Install dependency:
|
||||
|
||||
```bash
|
||||
pip install scapy
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
sudo -E python3 dhcpv4_option_probe.py --iface <interface> --request-options "1,3,6,15,42"
|
||||
```
|
||||
|
||||
Default output format:
|
||||
|
||||
```text
|
||||
RECEIVED_TAGS=1,3,6,15,42
|
||||
```
|
||||
|
||||
## Debug mode
|
||||
|
||||
Pass `--debug` to print the full scapy packet dump for each DHCP exchange to stderr:
|
||||
|
||||
```bash
|
||||
sudo -E python3 dhcpv4_option_probe.py --iface <interface> --request-options "1,3,6,15,42" --debug
|
||||
```
|
||||
|
||||
The debug output includes:
|
||||
|
||||
- `--- DHCPDISCOVER sent ---` — full packet before sending
|
||||
- `--- DHCPOFFER received ---` — full packet from server
|
||||
- `--- DHCPREQUEST sent ---` — full packet before sending
|
||||
- `--- DHCPACK received ---` — full packet from server
|
||||
- `DEBUG_DHCPREQUEST_PRL=<tags>` — the parameter request list sent in DHCPREQUEST (stdout)
|
||||
|
||||
Debug output goes to stderr so the `RECEIVED_TAGS=` line on stdout is unaffected.
|
||||
|
||||
## Real-environment recommendation (network delay tolerance)
|
||||
|
||||
Use a higher timeout when network delays are expected:
|
||||
|
||||
```bash
|
||||
sudo -E python3 dhcpv4_option_probe.py --iface <interface> --request-options "1,3,6,15,42" --timeout 12
|
||||
```
|
||||
|
||||
## Exit codes
|
||||
|
||||
- `0`: success (`DHCPACK` received and parsed)
|
||||
- `2`: error (`DHCPOFFER`/`DHCPACK` missing, `DHCPNAK`, or invalid DHCP response)
|
||||
@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Minimal DHCPv4 probe for Cram:
|
||||
- Sends DHCPDISCOVER with configurable Parameter Request List (PRL)
|
||||
- Sends DHCPREQUEST using the offered address and server identifier
|
||||
- Prints received option tags from final DHCPACK as:
|
||||
RECEIVED_TAGS=1,3,6,...
|
||||
- Optional debug output can print the PRL sent in DHCPREQUEST
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import random
|
||||
import sys
|
||||
|
||||
from scapy.all import BOOTP, DHCP, Ether, IP, UDP, conf, srp1 # type: ignore
|
||||
from scapy.layers.dhcp import DHCPRevOptions # type: ignore
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--iface", required=True, help="Interface used to send/receive DHCP")
|
||||
parser.add_argument("--request-options", default="", help="CSV option tags in PRL, e.g. 1,3,6,15,42")
|
||||
parser.add_argument("--timeout", type=int, default=8)
|
||||
parser.add_argument("--debug", action="store_true")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def parse_prl(csv_text):
|
||||
if not csv_text.strip():
|
||||
return []
|
||||
return [int(value.strip()) for value in csv_text.split(",") if value.strip()]
|
||||
|
||||
|
||||
def extract_option_tags(dhcp_options):
|
||||
tags = []
|
||||
for opt in dhcp_options:
|
||||
if not (isinstance(opt, tuple) and len(opt) >= 1):
|
||||
continue
|
||||
|
||||
name = opt[0]
|
||||
|
||||
if isinstance(name, int):
|
||||
tags.append(name)
|
||||
continue
|
||||
|
||||
if isinstance(name, str):
|
||||
code = DHCPRevOptions.get(name)
|
||||
if isinstance(code, int):
|
||||
tags.append(code)
|
||||
elif isinstance(code, tuple) and len(code) >= 1 and isinstance(code[0], int):
|
||||
tags.append(code[0])
|
||||
|
||||
return sorted(set(tags))
|
||||
|
||||
|
||||
def get_option_value(dhcp_options, option_name):
|
||||
for opt in dhcp_options:
|
||||
if isinstance(opt, tuple) and len(opt) >= 2 and opt[0] == option_name:
|
||||
return opt[1]
|
||||
return None
|
||||
|
||||
|
||||
def get_message_type(dhcp_options):
|
||||
value = get_option_value(dhcp_options, "message-type")
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
value_map = {
|
||||
"discover": 1,
|
||||
"offer": 2,
|
||||
"request": 3,
|
||||
"decline": 4,
|
||||
"ack": 5,
|
||||
"nak": 6,
|
||||
"release": 7,
|
||||
"inform": 8,
|
||||
}
|
||||
return value_map.get(value.lower())
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
conf.checkIPaddr = False
|
||||
|
||||
xid = random.randint(1, 0xFFFFFFFF)
|
||||
prl = parse_prl(args.request_options)
|
||||
|
||||
discover_packet = (
|
||||
Ether(dst="ff:ff:ff:ff:ff:ff")
|
||||
/ IP(src="0.0.0.0", dst="255.255.255.255")
|
||||
/ UDP(sport=68, dport=67)
|
||||
/ BOOTP(chaddr=b"\xaa\xbb\xcc\xdd\xee\xff", xid=xid, flags=0x8000)
|
||||
/ DHCP(options=[
|
||||
("message-type", "discover"),
|
||||
("param_req_list", prl),
|
||||
"end",
|
||||
])
|
||||
)
|
||||
|
||||
if args.debug:
|
||||
print("--- DHCPDISCOVER sent ---", file=sys.stderr)
|
||||
print(discover_packet.show(dump=True), file=sys.stderr)
|
||||
|
||||
offer = srp1(discover_packet, iface=args.iface, timeout=args.timeout, verbose=False)
|
||||
if offer is None or not offer.haslayer(DHCP):
|
||||
print("ERROR: no DHCPOFFER received", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
if args.debug:
|
||||
print("--- DHCPOFFER received ---", file=sys.stderr)
|
||||
print(offer.show(dump=True), file=sys.stderr)
|
||||
|
||||
if get_message_type(offer[DHCP].options) != 2:
|
||||
print("ERROR: received DHCP response is not DHCPOFFER", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
offered_ip = offer[BOOTP].yiaddr
|
||||
server_identifier = get_option_value(offer[DHCP].options, "server_id")
|
||||
|
||||
request_options = [
|
||||
("message-type", "request"),
|
||||
("requested_addr", offered_ip),
|
||||
("param_req_list", prl),
|
||||
]
|
||||
if args.debug:
|
||||
print("DEBUG_DHCPREQUEST_PRL=" + ",".join(str(tag) for tag in prl))
|
||||
if server_identifier is not None:
|
||||
request_options.append(("server_id", server_identifier))
|
||||
request_options.append("end")
|
||||
|
||||
request_packet = (
|
||||
Ether(dst="ff:ff:ff:ff:ff:ff")
|
||||
/ IP(src="0.0.0.0", dst="255.255.255.255")
|
||||
/ UDP(sport=68, dport=67)
|
||||
/ BOOTP(chaddr=b"\xaa\xbb\xcc\xdd\xee\xff", xid=xid, flags=0x8000)
|
||||
/ DHCP(options=request_options)
|
||||
)
|
||||
|
||||
if args.debug:
|
||||
print("--- DHCPREQUEST sent ---", file=sys.stderr)
|
||||
print(request_packet.show(dump=True), file=sys.stderr)
|
||||
|
||||
final_reply = srp1(request_packet, iface=args.iface, timeout=args.timeout, verbose=False)
|
||||
if final_reply is None or not final_reply.haslayer(DHCP):
|
||||
print("ERROR: no DHCPACK received", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
if args.debug:
|
||||
print("--- DHCPACK received ---", file=sys.stderr)
|
||||
print(final_reply.show(dump=True), file=sys.stderr)
|
||||
|
||||
message_type = get_message_type(final_reply[DHCP].options)
|
||||
if message_type == 6:
|
||||
print("ERROR: DHCPNAK received", file=sys.stderr)
|
||||
return 2
|
||||
if message_type != 5:
|
||||
print("ERROR: final DHCP response is not DHCPACK", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
tags = extract_option_tags(final_reply[DHCP].options)
|
||||
print("RECEIVED_TAGS=" + ",".join(str(tag) for tag in tags))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -224,9 +224,9 @@ Device.DHCPv4.Server.Pool.{i}.MinAddress
|
||||
Device.DHCPv4.Server.Pool.{i}.Option.{i}. proto::dhcpv4-manager
|
||||
Device.DHCPv4.Server.Pool.{i}.Option.{i}.Alias proto::dhcpv4-manager
|
||||
Device.DHCPv4.Server.Pool.{i}.Option.{i}.Enable proto::dhcpv4-manager
|
||||
Device.DHCPv4.Server.Pool.{i}.Option.{i}.Force proto::dhcpv4-manager
|
||||
Device.DHCPv4.Server.Pool.{i}.Option.{i}.Tag proto::dhcpv4-manager
|
||||
Device.DHCPv4.Server.Pool.{i}.Option.{i}.Value proto::dhcpv4-manager
|
||||
Device.DHCPv4.Server.Pool.{i}.Option.{i}.X_PRPLWARE-COM_Force proto::dhcpv4-manager
|
||||
Device.DHCPv4.Server.Pool.{i}.OptionNumberOfEntries proto::dhcpv4-manager
|
||||
Device.DHCPv4.Server.Pool.{i}.Order proto::dhcpv4-manager
|
||||
Device.DHCPv4.Server.Pool.{i}.ReservedAddresses proto::dhcpv4-manager
|
||||
|
||||
Loading…
Reference in New Issue
Block a user