Skip to content

Commit 267ceba

Browse files
committed
fix: don't allow random IPs
This is another guard against potentially corrupted data in Nautobot.
1 parent b9ee849 commit 267ceba

File tree

2 files changed

+189
-5
lines changed

2 files changed

+189
-5
lines changed

python/understack-workflows/tests/test_netapp_value_objects.py

Lines changed: 180 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -436,10 +436,10 @@ def test_from_nexthop_ip_third_octet_128(self):
436436
def test_from_nexthop_ip_various_valid_ips(self):
437437
"""Test route destination calculation with various valid IP patterns."""
438438
test_cases = [
439-
("192.168.0.1", "100.126.0.0/17"),
440-
("10.0.0.254", "100.126.0.0/17"),
441-
("172.16.128.1", "100.126.128.0/17"),
442-
("203.0.128.100", "100.126.128.0/17"),
439+
("100.64.0.1", "100.126.0.0/17"),
440+
("100.65.0.254", "100.126.0.0/17"),
441+
("100.66.128.1", "100.126.128.0/17"),
442+
("100.67.128.100", "100.126.128.0/17"),
443443
]
444444

445445
for nexthop_ip, expected_destination in test_cases:
@@ -490,6 +490,182 @@ def test_calculate_destination_invalid_ip_format(self):
490490
with pytest.raises((ValueError, ipaddress.AddressValueError)):
491491
RouteSpec._calculate_destination(invalid_format)
492492

493+
def test_calculate_destination_comprehensive_third_octet_zero(self):
494+
"""Test comprehensive route destination calculation for third octet = 0."""
495+
# Test various IP addresses with third octet = 0 within 100.64.0.0/10 subnet
496+
test_ips = [
497+
"100.64.0.1",
498+
"100.65.0.1",
499+
"100.66.0.254",
500+
"100.67.0.100",
501+
"100.68.0.3",
502+
"100.127.0.255",
503+
]
504+
505+
for ip in test_ips:
506+
destination = RouteSpec._calculate_destination(ip)
507+
assert destination == "100.126.0.0/17", f"Failed for IP: {ip}"
508+
509+
def test_calculate_destination_comprehensive_third_octet_128(self):
510+
"""Test comprehensive route destination calculation for third octet = 128."""
511+
# Test various IP addresses with third octet = 128 within 100.64.0.0/10 subnet
512+
test_ips = [
513+
"100.64.128.1",
514+
"100.65.128.1",
515+
"100.66.128.254",
516+
"100.67.128.100",
517+
"100.68.128.3",
518+
"100.127.128.255",
519+
]
520+
521+
for ip in test_ips:
522+
destination = RouteSpec._calculate_destination(ip)
523+
assert destination == "100.126.128.0/17", f"Failed for IP: {ip}"
524+
525+
def test_calculate_destination_comprehensive_invalid_patterns(self):
526+
"""Test comprehensive error handling for all invalid third octet values."""
527+
# Test all invalid third octet values (not 0 or 128) within 100.64.0.0/10 subnet
528+
invalid_third_octets = [1, 2, 63, 64, 127, 129, 192, 254, 255]
529+
530+
for third_octet in invalid_third_octets:
531+
invalid_ip = f"100.64.{third_octet}.1"
532+
with pytest.raises(ValueError, match="Unsupported IP pattern"):
533+
RouteSpec._calculate_destination(invalid_ip)
534+
535+
def test_from_nexthop_ip_comprehensive_valid_patterns(self):
536+
"""Test RouteSpec.from_nexthop_ip with comprehensive valid IP patterns."""
537+
# Test cases: (nexthop_ip, expected_destination) - all within 100.64.0.0/10
538+
test_cases = [
539+
# Third octet = 0 cases
540+
("100.64.0.1", "100.126.0.0/17"),
541+
("100.65.0.254", "100.126.0.0/17"),
542+
("100.66.0.100", "100.126.0.0/17"),
543+
("100.67.0.50", "100.126.0.0/17"),
544+
("100.127.0.17", "100.126.0.0/17"), # From design document example
545+
# Third octet = 128 cases
546+
("100.64.128.1", "100.126.128.0/17"),
547+
("100.65.128.254", "100.126.128.0/17"),
548+
("100.66.128.100", "100.126.128.0/17"),
549+
("100.67.128.50", "100.126.128.0/17"),
550+
("100.127.128.17", "100.126.128.0/17"), # From design document example
551+
]
552+
553+
svm_name = "os-550e8400-e29b-41d4-a716-446655440000" # Valid UUID format
554+
555+
for nexthop_ip, expected_destination in test_cases:
556+
spec = RouteSpec.from_nexthop_ip(svm_name, nexthop_ip)
557+
assert spec.svm_name == svm_name
558+
assert spec.gateway == nexthop_ip
559+
assert spec.destination == expected_destination
560+
561+
def test_from_nexthop_ip_comprehensive_invalid_patterns(self):
562+
"""Test RouteSpec.from_nexthop_ip with comprehensive invalid IP patterns."""
563+
svm_name = "os-550e8400-e29b-41d4-a716-446655440000"
564+
565+
# Test invalid third octet values within 100.64.0.0/10 subnet
566+
invalid_third_octets = [1, 2, 63, 64, 127, 129, 192, 254, 255]
567+
568+
for third_octet in invalid_third_octets:
569+
invalid_ip = f"100.64.{third_octet}.1"
570+
with pytest.raises(ValueError, match="Unsupported IP pattern"):
571+
RouteSpec.from_nexthop_ip(svm_name, invalid_ip)
572+
573+
def test_from_nexthop_ip_edge_cases(self):
574+
"""Test RouteSpec.from_nexthop_ip with edge case IP addresses."""
575+
svm_name = "os-550e8400-e29b-41d4-a716-446655440000"
576+
577+
# Edge cases for third octet = 0 within 100.64.0.0/10 subnet
578+
edge_cases_zero = [
579+
"100.64.0.1",
580+
"100.127.0.1",
581+
"100.65.0.1",
582+
]
583+
584+
for ip in edge_cases_zero:
585+
spec = RouteSpec.from_nexthop_ip(svm_name, ip)
586+
assert spec.destination == "100.126.0.0/17"
587+
assert spec.gateway == ip
588+
589+
# Edge cases for third octet = 128 within 100.64.0.0/10 subnet
590+
edge_cases_128 = [
591+
"100.64.128.1",
592+
"100.127.128.255",
593+
"100.65.128.1",
594+
]
595+
596+
for ip in edge_cases_128:
597+
spec = RouteSpec.from_nexthop_ip(svm_name, ip)
598+
assert spec.destination == "100.126.128.0/17"
599+
assert spec.gateway == ip
600+
601+
def test_calculate_destination_boundary_values(self):
602+
"""Test _calculate_destination with boundary values for third octet."""
603+
# Test exact boundary values within 100.64.0.0/10 subnet
604+
assert RouteSpec._calculate_destination("100.64.0.1") == "100.126.0.0/17"
605+
assert RouteSpec._calculate_destination("100.64.128.1") == "100.126.128.0/17"
606+
607+
# Test values just outside boundaries should fail
608+
with pytest.raises(ValueError, match="Unsupported IP pattern"):
609+
RouteSpec._calculate_destination("100.64.1.1") # Just above 0
610+
611+
with pytest.raises(ValueError, match="Unsupported IP pattern"):
612+
RouteSpec._calculate_destination("100.64.127.1") # Just below 128
613+
614+
with pytest.raises(ValueError, match="Unsupported IP pattern"):
615+
RouteSpec._calculate_destination("100.64.129.1") # Just above 128
616+
617+
def test_calculate_destination_subnet_validation(self):
618+
"""Test _calculate_destination validates IP is within 100.64.0.0/10 subnet."""
619+
# Test IPs outside 100.64.0.0/10 subnet should fail
620+
invalid_subnet_ips = [
621+
"192.168.0.1", # Private network
622+
"10.0.0.1", # Private network
623+
"172.16.0.1", # Private network
624+
"8.8.8.8", # Public network
625+
"100.63.0.1", # Just below 100.64.0.0/10
626+
"100.128.0.1", # Just above 100.64.0.0/10
627+
"101.64.0.1", # Outside range
628+
"99.64.0.1", # Outside range
629+
]
630+
631+
for invalid_ip in invalid_subnet_ips:
632+
with pytest.raises(ValueError, match="not within required 100.64.0.0/10"):
633+
RouteSpec._calculate_destination(invalid_ip)
634+
635+
def test_from_nexthop_ip_subnet_validation(self):
636+
"""Test RouteSpec.from_nexthop_ip validates IP is within 100.64.0.0/10."""
637+
svm_name = "os-550e8400-e29b-41d4-a716-446655440000"
638+
639+
# Test IPs outside 100.64.0.0/10 subnet should fail
640+
invalid_subnet_ips = [
641+
"192.168.0.1", # Private network
642+
"10.0.0.1", # Private network
643+
"172.16.128.1", # Private network
644+
"8.8.8.8", # Public network
645+
"100.63.0.1", # Just below 100.64.0.0/10
646+
"100.128.0.1", # Just above 100.64.0.0/10
647+
]
648+
649+
for invalid_ip in invalid_subnet_ips:
650+
with pytest.raises(ValueError, match="not within required 100.64.0.0/10"):
651+
RouteSpec.from_nexthop_ip(svm_name, invalid_ip)
652+
653+
def test_calculate_destination_valid_subnet_range(self):
654+
"""Test _calculate_destination accepts valid IPs within 100.64.0.0/10 subnet."""
655+
# Test boundary IPs within 100.64.0.0/10 subnet
656+
valid_subnet_ips = [
657+
("100.64.0.1", "100.126.0.0/17"), # Start of range, third octet 0
658+
("100.64.128.1", "100.126.128.0/17"), # Start of range, third octet 128
659+
("100.127.0.1", "100.126.0.0/17"), # End of range, third octet 0
660+
("100.127.128.1", "100.126.128.0/17"), # End of range, third octet 128
661+
("100.65.0.100", "100.126.0.0/17"), # Middle of range, third octet 0
662+
("100.66.128.200", "100.126.128.0/17"), # Middle of range, third octet 128
663+
]
664+
665+
for valid_ip, expected_destination in valid_subnet_ips:
666+
destination = RouteSpec._calculate_destination(valid_ip)
667+
assert destination == expected_destination
668+
493669

494670
class TestRouteResult:
495671
"""Test cases for RouteResult value object."""

python/understack-workflows/understack_workflows/netapp/value_objects.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,17 @@ def _calculate_destination(nexthop_ip: str) -> str:
212212
str: Route destination in CIDR format
213213
214214
Raises:
215-
ValueError: If IP pattern is not supported (third octet not 0 or 128)
215+
ValueError: If IP is not in 100.64.0.0/10 subnet or third octet not 0 or 128
216216
"""
217217
ip = ipaddress.IPv4Address(nexthop_ip)
218+
219+
# Validate that IP is within 100.64.0.0/10 subnet
220+
carrier_grade_nat_network = ipaddress.IPv4Network("100.64.0.0/10")
221+
if ip not in carrier_grade_nat_network:
222+
raise ValueError(
223+
f"IP address {nexthop_ip} is not within required 100.64.0.0/10 subnet"
224+
)
225+
218226
third_octet = int(str(ip).split(".")[2])
219227

220228
if third_octet == 0:

0 commit comments

Comments
 (0)