|
| 1 | +#!/usr/bin/perl |
| 2 | +use strict; |
| 3 | +use warnings; |
| 4 | +use IO::Socket::INET; |
| 5 | +use IO::Select; |
| 6 | +use Time::HiRes qw(sleep time); |
| 7 | + |
| 8 | +if (@ARGV != 7) { |
| 9 | + die "Usage: $0 local_port dest_ip dest_port DNP3_src DNP3_dst count duration_sec\n"; |
| 10 | +} |
| 11 | + |
| 12 | +my ($local_port, $dest_ip, $dest_port, $DNP3_src, $DNP3_dst, $count, $duration) = @ARGV; |
| 13 | + |
| 14 | +for my $p ($local_port, $dest_port) { |
| 15 | + die "Port must be integer 0..65535\n" unless $p =~ /^\d+$/ && $p >= 0 && $p <= 65535; |
| 16 | +} |
| 17 | +for my $v ($DNP3_src, $DNP3_dst) { |
| 18 | + die "DNP3 addr values must be integer 0..65535\n" unless $v =~ /^\d+$/ && $v >= 0 && $v <= 65535; |
| 19 | +} |
| 20 | + |
| 21 | +#printf("DNP3_src = 0x%04X\n", $DNP3_src); |
| 22 | +#printf("DNP3_dst = 0x%04X\n", $DNP3_dst); |
| 23 | + |
| 24 | +# Build payload |
| 25 | +my $DNP3_start = 0x0564; |
| 26 | +my $DNP3_ctl_len = 0x05c9; |
| 27 | +my $payload = pack('n n v v', $DNP3_start, $DNP3_ctl_len, $DNP3_dst, $DNP3_src); |
| 28 | + |
| 29 | +# Compute CRC |
| 30 | +my $crc = computeCRC($payload); |
| 31 | + |
| 32 | +# Debug: print CRC |
| 33 | +#printf("CRC = 0x%04X\n", $crc); |
| 34 | + |
| 35 | +# Append CRC |
| 36 | +$payload .= pack('v', $crc); |
| 37 | + |
| 38 | +# Debug: print final buffer as hex |
| 39 | +#print "Final buffer (hex): "; |
| 40 | +#print unpack("H*", $payload), "\n"; |
| 41 | + |
| 42 | +# Create UDP socket |
| 43 | +my $sock = IO::Socket::INET->new( |
| 44 | + Proto => 'udp', |
| 45 | + LocalPort => $local_port, |
| 46 | + PeerAddr => $dest_ip, |
| 47 | + PeerPort => $dest_port, |
| 48 | +) or die "Cannot create UDP socket: $!\n"; |
| 49 | + |
| 50 | +# Statistics tracking |
| 51 | +my %packet_counts; # Key: "src:dst", Value: count |
| 52 | +my $total_received = 0; |
| 53 | +my $total_sent = 0; |
| 54 | +my $crc_errors = 0; |
| 55 | + |
| 56 | +# Create select object for non-blocking reads |
| 57 | +my $select = IO::Select->new($sock); |
| 58 | + |
| 59 | +# Fork to handle sending and receiving concurrently |
| 60 | +my $pid = fork(); |
| 61 | +die "Fork failed: $!\n" unless defined $pid; |
| 62 | + |
| 63 | +if ($pid == 0) { |
| 64 | + # Child process: Send packets |
| 65 | + for my $i (1..$count) { |
| 66 | + my $sent = $sock->send($payload); |
| 67 | + if (defined $sent) { |
| 68 | + #print "Sent packet $i ($sent bytes) to $dest_ip:$dest_port\n"; |
| 69 | + $total_sent++; |
| 70 | + } else { |
| 71 | + warn "Send $i failed: $!\n"; |
| 72 | + } |
| 73 | + sleep($duration/$count) if $i < $count; # Don't sleep after last packet |
| 74 | + } |
| 75 | + exit(0); |
| 76 | +} else { |
| 77 | + # Parent process: Receive packets |
| 78 | + my $last_packet_time = time(); |
| 79 | + my $timeout = 2.0; # 2 seconds of silence |
| 80 | + |
| 81 | + while (1) { |
| 82 | + my $elapsed = time() - $last_packet_time; |
| 83 | + my $remaining = $timeout - $elapsed; |
| 84 | + |
| 85 | + # Check if we've had 2 seconds of silence |
| 86 | + if ($remaining <= 0) { |
| 87 | + last; |
| 88 | + } |
| 89 | + |
| 90 | + # Wait for data with remaining timeout |
| 91 | + my @ready = $select->can_read($remaining); |
| 92 | + |
| 93 | + if (@ready) { |
| 94 | + my $recv_data; |
| 95 | + my $peer_addr = $sock->recv($recv_data, 1024); |
| 96 | + |
| 97 | + if (defined $peer_addr) { |
| 98 | + $last_packet_time = time(); |
| 99 | + $total_received++; |
| 100 | + |
| 101 | + my ($peer_port, $peer_ip) = sockaddr_in($peer_addr); |
| 102 | + my $peer_ip_str = inet_ntoa($peer_ip); |
| 103 | + |
| 104 | + # Parse the received packet |
| 105 | + if (length($recv_data) >= 10) { |
| 106 | + my ($start, $ctl_len, $dst, $src, $recv_crc) = unpack('n n v v v', $recv_data); |
| 107 | + |
| 108 | + # Verify CRC |
| 109 | + my $data_for_crc = substr($recv_data, 0, 8); |
| 110 | + my $computed_crc = computeCRC($data_for_crc); |
| 111 | + |
| 112 | + if ($computed_crc == $recv_crc) { |
| 113 | + #printf("Received valid packet from %s:%d - DNP3 src=0x%04X dst=0x%04X\n", |
| 114 | + # $peer_ip_str, $peer_port, $src, $dst); |
| 115 | + |
| 116 | + # Track statistics |
| 117 | + my $key = sprintf("0x%04X->0x%04X", $src, $dst); |
| 118 | + $packet_counts{$key}++; |
| 119 | + } else { |
| 120 | + printf("Received packet with CRC error from %s:%d (expected 0x%04X, got 0x%04X)\n", |
| 121 | + $peer_ip_str, $peer_port, $computed_crc, $recv_crc); |
| 122 | + $crc_errors++; |
| 123 | + } |
| 124 | + } else { |
| 125 | + printf("Received short packet from %s:%d (%d bytes)\n", |
| 126 | + $peer_ip_str, $peer_port, length($recv_data)); |
| 127 | + } |
| 128 | + } |
| 129 | + } |
| 130 | + } |
| 131 | + |
| 132 | + # Wait for child to finish |
| 133 | + waitpid($pid, 0); |
| 134 | + |
| 135 | + for my $addr (sort {$packet_counts{$b}<=>$packet_counts{$a}} keys %packet_counts) { |
| 136 | + printf("%s : %d\n", $addr, $packet_counts{$addr}); |
| 137 | + } |
| 138 | + $sock->close(); |
| 139 | +} |
| 140 | + |
| 141 | + |
| 142 | +# ----------------- CRC FUNCTION ----------------- |
| 143 | +sub computeCRC { |
| 144 | + my ($buf) = @_; |
| 145 | + my @bytes = unpack('C*', $buf); |
| 146 | + |
| 147 | + my $crc = 0; |
| 148 | + for my $dataOctet (@bytes) { |
| 149 | + for (0..7) { |
| 150 | + my $temp = ($crc ^ $dataOctet) & 1; |
| 151 | + $crc >>= 1; |
| 152 | + $dataOctet >>= 1; |
| 153 | + if ($temp) { |
| 154 | + $crc ^= 0xA6BC; |
| 155 | + } |
| 156 | + } |
| 157 | + } |
| 158 | + $crc = (~$crc) & 0xFFFF; |
| 159 | + return $crc; |
| 160 | +} |
0 commit comments