diff --git a/tests/10-socket.t b/tests/10-socket.t index 64188f9..02c5cfe 100644 --- a/tests/10-socket.t +++ b/tests/10-socket.t @@ -1,5 +1,5 @@ # IPXWrapper test suite -# Copyright (C) 2014 Daniel Collins +# Copyright (C) 2014-2023 Daniel Collins # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 as published by @@ -22,12 +22,15 @@ use Test::Spec; use FindBin; use lib "$FindBin::Bin/lib/"; +use IPXWrapper::DOSBoxServer; use IPXWrapper::Util; require "$FindBin::Bin/config.pm"; +our ($local_ip_a); our ($remote_mac_a, $remote_ip_a); our ($remote_mac_b, $remote_ip_b); +our ($dosbox_port); # NOTE: These constants are the values used on Windows, not the host. use constant { @@ -142,6 +145,48 @@ describe "IPXWrapper" => sub like($output, qr/^socket: -1$/m); }; }; + + describe "using a DOSBox server" => sub + { + my $dosbox_server; + + before all => sub + { + reg_delete_key($remote_ip_a, "HKCU\\Software\\IPXWrapper"); + reg_set_dword( $remote_ip_a, "HKCU\\Software\\IPXWrapper", "use_pcap", ENCAP_TYPE_DOSBOX); + reg_set_string($remote_ip_a, "HKCU\\Software\\IPXWrapper", "dosbox_server_addr", $local_ip_a); + reg_set_dword( $remote_ip_a, "HKCU\\Software\\IPXWrapper", "dosbox_server_port", $dosbox_port); + + # $dosbox_server = IPXWrapper::Tool::DOSBoxServer->new($dosbox_port); + }; + + after all => sub + { + $dosbox_server = undef; + }; + + it_should_behave_like "socket initialisation"; + + it "socket(AF_IPX, SOCK_STREAM, NSPROTO_SPX) fails" => sub + { + my $output = run_remote_cmd( + $remote_ip_a, "Z:\\tools\\socket.exe", + AF_IPX, SOCK_STREAM, NSPROTO_SPX, + ); + + like($output, qr/^socket: -1$/m); + }; + + it "socket(AF_IPX, SOCK_STREAM, NSPROTO_SPXII) fails" => sub + { + my $output = run_remote_cmd( + $remote_ip_a, "Z:\\tools\\socket.exe", + AF_IPX, SOCK_STREAM, NSPROTO_SPXII, + ); + + like($output, qr/^socket: -1$/m); + }; + }; }; runtests unless caller; diff --git a/tests/15-interfaces.t b/tests/15-interfaces.t index b968877..2fe8033 100644 --- a/tests/15-interfaces.t +++ b/tests/15-interfaces.t @@ -22,18 +22,22 @@ use Test::Spec; use FindBin; use lib "$FindBin::Bin/lib/"; +use IPXWrapper::DOSBoxServer; use IPXWrapper::Tool::Bind; use IPXWrapper::Util; require "$FindBin::Bin/config.pm"; +our ($local_ip_a); our ($remote_mac_a, $remote_ip_a); our ($remote_mac_b, $remote_ip_b); +our ($dosbox_port); use constant { - IP_MAX_DATA_SIZE => 8192, - ETHER_MAX_DATA_SIZE => 1470, - LLC_MAX_DATA_SIZE => 1467, + IP_MAX_DATA_SIZE => 8192, + ETHER_MAX_DATA_SIZE => 1470, + LLC_MAX_DATA_SIZE => 1467, + DOSBOX_MAX_DATA_SIZE => 1424, }; my @expected_addrs; @@ -64,13 +68,14 @@ shared_examples_for "getsockopt" => sub reg_set_addr($remote_ip_a, "HKCU\\Software\\IPXWrapper", "primary", $remote_mac_b); my $first_b = get_first_addr_node() // ""; - ok($first_a eq $remote_mac_a && $first_b eq $remote_mac_b); + is($first_a, $remote_mac_a); + is($first_b, $remote_mac_b); }; }; describe "IPXWrapper" => sub { - describe "using IP encapsulation" => sub + describe "using IPXWrapper IP encapsulation" => sub { before all => sub { @@ -260,6 +265,57 @@ describe "IPXWrapper" => sub it_should_behave_like "getsockopt"; }; }; + + describe "using DOSBox UDP encapsulation" => sub + { + my $dosbox_server; + + before all => sub + { + reg_delete_key($remote_ip_a, "HKCU\\Software\\IPXWrapper"); + reg_set_dword( $remote_ip_a, "HKCU\\Software\\IPXWrapper", "use_pcap", ENCAP_TYPE_DOSBOX); + reg_set_string($remote_ip_a, "HKCU\\Software\\IPXWrapper", "dosbox_server_addr", $local_ip_a); + reg_set_dword( $remote_ip_a, "HKCU\\Software\\IPXWrapper", "dosbox_server_port", $dosbox_port); + + $dosbox_server = IPXWrapper::DOSBoxServer->new($dosbox_port); + + @expected_addrs = ( + { + # The node number is randomly selected by the DOSBox server + # when each client connects. + + net => "00:00:00:00", + maxpkt => DOSBOX_MAX_DATA_SIZE, + }, + ); + }; + + after all => sub + { + $dosbox_server = undef; + }; + + # Duplicate of common getsockopt block to skip the default interface selection + # logic test (because there is only ever one interface here). + describe "getsockopt" => sub + { + it "returns correct addresses" => sub + { + my @addrs = getsockopt_interfaces($remote_ip_a); + cmp_hashes_partial(\@addrs, \@expected_addrs); + }; + + it "returns correct IPX_MAX_ADAPTER_NUM" => sub + { + my @addrs = getsockopt_interfaces($remote_ip_a); + + my $output = run_remote_cmd($remote_ip_a, "Z:\\tools\\list-interfaces.exe"); + my ($got_num) = ($output =~ m/^IPX_MAX_ADAPTER_NUM = (\d+)$/m); + + is($got_num, (scalar @addrs)); + }; + }; + }; }; sub get_first_addr_node diff --git a/tests/30-dosbox-ipx.t b/tests/30-dosbox-ipx.t new file mode 100644 index 0000000..037a8be --- /dev/null +++ b/tests/30-dosbox-ipx.t @@ -0,0 +1,180 @@ +# IPXWrapper test suite +# Copyright (C) 2023 Daniel Collins +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 as published by +# the Free Software Foundation. +# +# This program 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +use strict; +use warnings; + +use Test::Spec; + +use FindBin; +use lib "$FindBin::Bin/lib/"; + +use IPXWrapper::DOSBoxClient; +use IPXWrapper::DOSBoxServer; +use IPXWrapper::Tool::IPXISR; +use IPXWrapper::Tool::IPXRecv; +use IPXWrapper::Util; + +require "$FindBin::Bin/config.pm"; + +our ($local_dev_a, $local_mac_a, $local_ip_a); +our ($local_dev_b, $local_mac_b, $local_ip_b); +our ($remote_mac_a, $remote_ip_a); +our ($remote_mac_b, $remote_ip_b); +our ($net_a_bcast, $net_b_bcast); +our ($dosbox_port); + +require "$FindBin::Bin/ptype.pm"; + +our $ptype_send_func; +our $ptype_capture_class; + +describe "IPXWrapper using DOSBox UDP encapsulation" => sub +{ + my $dosbox_server; + + before all => sub + { + reg_delete_key($remote_ip_a, "HKCU\\Software\\IPXWrapper"); + reg_set_dword( $remote_ip_a, "HKCU\\Software\\IPXWrapper", "use_pcap", ENCAP_TYPE_DOSBOX); + reg_set_string($remote_ip_a, "HKCU\\Software\\IPXWrapper", "dosbox_server_addr", $local_ip_a); + reg_set_dword( $remote_ip_a, "HKCU\\Software\\IPXWrapper", "dosbox_server_port", $dosbox_port); + + $dosbox_server = IPXWrapper::DOSBoxServer->new($dosbox_port); + }; + + after all => sub + { + $dosbox_server = undef; + }; + + it "handles unicast packets from the server" => sub + { + my $capture_a = IPXWrapper::Tool::IPXRecv->new( + $remote_ip_a, + "-b", "-r", "00:00:00:00", "00:00:00:00:00:00", "4444", + ); + + my $client = IPXWrapper::DOSBoxClient->new($local_ip_a, $dosbox_port); + + note("Their node number is ".$capture_a->node()); + note("My node number is ".$client->node()); + + $client->send( + tc => 0, + type => 0, + + dest_network => "00:00:00:00", + dest_node => $capture_a->node(), + dest_socket => 4444, + + src_network => $client->net(), + src_node => $client->node(), + src_socket => 1234, + + data => "damage", + ); + + sleep(1); + + my @packets_a = $capture_a->kill_and_read(); + + cmp_hashes_partial(\@packets_a, [ + { + src_network => "00:00:00:00", + src_node => $client->node(), + src_socket => 1234, + + data => "damage", + }, + ]); + }; + + it "handles broadcast packets from the server" => sub + { + my $capture_a = IPXWrapper::Tool::IPXRecv->new( + $remote_ip_a, + "-b", "-r", "00:00:00:00", "00:00:00:00:00:00", "4444", + ); + + my $client = IPXWrapper::DOSBoxClient->new($local_ip_a, $dosbox_port); + + note("Their node number is ".$capture_a->node()); + note("My node number is ".$client->node()); + + $client->send( + tc => 0, + type => 0, + + dest_network => "00:00:00:00", + dest_node => "FF:FF:FF:FF:FF:FF", + dest_socket => 4444, + + src_network => $client->net(), + src_node => $client->node(), + src_socket => 1234, + + data => "location", + ); + + sleep(1); + + my @packets_a = $capture_a->kill_and_read(); + + cmp_hashes_partial(\@packets_a, [ + { + src_network => "00:00:00:00", + src_node => $client->node(), + src_socket => 1234, + + data => "location", + }, + ]); + }; + + it "sends unicast packets to server" => sub + { + my $client = IPXWrapper::DOSBoxClient->new($local_ip_a, $dosbox_port); + + note("My node number is ".$client->node()); + + run_remote_cmd( + $remote_ip_a, "Z:\\tools\\ipx-send.exe", + "-d" => "tasty", + "-s" => "5555", + "00:00:00:00", $client->node(), "4444", + ); + + sleep(1); + + my @packets = $client->recv_any(); + + cmp_hashes_partial(\@packets, [ + { + src_network => "00:00:00:00", + src_socket => 5555, + + dest_network => "00:00:00:00", + dest_node => $client->node(), + dest_socket => 4444, + + data => "tasty", + }, + ]); + }; +}; + +runtests unless caller; diff --git a/tests/config.pm b/tests/config.pm index 9cbd24b..5077b08 100644 --- a/tests/config.pm +++ b/tests/config.pm @@ -29,4 +29,8 @@ our $remote_c_ip = "172.16.1.23"; our $net_a_bcast = "172.16.1.255"; our $net_b_bcast = "172.16.2.255"; +# Port to use for test DOSBox server. + +our $dosbox_port = 8086; + 1; diff --git a/tests/lib/IPXWrapper/DOSBoxClient.pm b/tests/lib/IPXWrapper/DOSBoxClient.pm new file mode 100644 index 0000000..a26fcb8 --- /dev/null +++ b/tests/lib/IPXWrapper/DOSBoxClient.pm @@ -0,0 +1,113 @@ +# IPXWrapper test suite +# Copyright (C) 2023 Daniel Collins +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 as published by +# the Free Software Foundation. +# +# This program 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +use strict; +use warnings; + +package IPXWrapper::DOSBoxClient; + +use IO::Select; +use IO::Socket::INET; +use NetPacket::IPX; +use Test::Spec; + +sub new +{ + my ($class, $host, $port) = @_; + + my $sock = IO::Socket::INET->new( + Proto => "udp", + PeerAddr => $host, + PeerPort => $port, + ) or die("Can't create socket: $!"); + + my $reg_req = NetPacket::IPX->new( + tc => 0, + type => 2, + + src_network => "00:00:00:00", + src_node => "00:00:00:00:00:00", + src_socket => 2, + + dest_network => "00:00:00:00", + dest_node => "00:00:00:00:00:00", + dest_socket => 2, + + data => "", + ); + + $sock->send($reg_req->encode()) + or die("Can't send data: $!"); + + my $buf; + $sock->recv($buf, 1024, 0); + + my $reg_response = NetPacket::IPX->decode($buf); + + my $self = bless({ + sock => $sock, + + net => $reg_response->{dest_network}, + node => $reg_response->{dest_node}, + }, $class); + + return $self; +} + +sub net +{ + my ($self) = @_; + return $self->{net}; +} + +sub node +{ + my ($self) = @_; + return $self->{node}; +} + +sub send +{ + my ($self, %options) = @_; + + my $packet = NetPacket::IPX->new(%options); + my $enc_packet = $packet->encode(); + + $self->{sock}->send($enc_packet) + or die("Can't send data: $!"); +} + +sub recv_any +{ + my ($self) = @_; + + my @packets = (); + + my $select = IO::Select->new($self->{sock}); + + while($select->can_read(0)) + { + my $buf; + $self->{sock}->recv($buf, 2048); + + my $pkt = NetPacket::IPX->decode($buf); + push(@packets, $pkt); + } + + return @packets; +} + +1; diff --git a/tests/lib/IPXWrapper/DOSBoxServer.pm b/tests/lib/IPXWrapper/DOSBoxServer.pm new file mode 100644 index 0000000..f4d33b4 --- /dev/null +++ b/tests/lib/IPXWrapper/DOSBoxServer.pm @@ -0,0 +1,112 @@ +# IPXWrapper test suite +# Copyright (C) 2023 Daniel Collins +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 as published by +# the Free Software Foundation. +# +# This program 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +use strict; +use warnings; + +package IPXWrapper::DOSBoxServer; + +use File::Temp; +use IPC::Open3; +use POSIX qw(:signal_h); +use Proc::ProcessTable; +use Test::Spec; + +sub new +{ + my ($class, $port) = @_; + + my $dosbox_conf = File::Temp->new(); + print {$dosbox_conf} < "/dev/stdout", + "stdbuf", "-i0", "-o0", "-e0", + "dosbox", "-conf" => "$dosbox_conf"); + + note(join(" ", @command)); + + # No need for error checking here - open3 throws on failure. + my $pid = open3(my $in, my $out, undef, @command); + + my $self = bless({ + xvfb_run_pid => $pid, + }, $class); + + my $output = ""; + + while(defined(my $line = <$out>)) + { + $output .= $line; + + $line =~ s/[\r\n]//g; + + if($line =~ m/^IPX: Connected to server\./) + { + # We can't kill DOSBox by sending a signal to xvfb-run since it doesn't + # catch any signals and just dies, leaving orphan Xvfb and dosbox processes + # running. + # + # So we have to walk the process table and find the dosbox process owned by + # our xvfb-run process, which we can then directly kill and xvfb-run will + # clean up the Xvfb instance. + + my @procs = @{ Proc::ProcessTable->new()->table() }; + + my $find_dosbox = sub + { + my ($find_dosbox, $ppid) = @_; + + foreach my $child(grep { $_->ppid() == $ppid } @procs) + { + # if($child->cmdline->[0] eq "dosbox") + if($child->cmndline =~ m/^dosbox /) + { + return $child->pid; + } + + my $dosbox_pid = $find_dosbox->($find_dosbox, $child->pid); + return $dosbox_pid if(defined $dosbox_pid); + } + + return; + }; + + $self->{dosbox_pid} = $find_dosbox->($find_dosbox, $pid) + // die "Couldn't find DOSBox PID"; + + return $self; + } + } + + die("Didn't get expected output from xvfb-run/dosbox:\n$output"); +} + +sub DESTROY +{ + my ($self) = @_; + + # Kill DOSBox, then wait for xvfb-run to clean up. + kill(SIGTERM, $self->{dosbox_pid}); + waitpid($self->{xvfb_run_pid}, 0); +} + +1; diff --git a/tests/lib/IPXWrapper/Tool/IPXRecv.pm b/tests/lib/IPXWrapper/Tool/IPXRecv.pm index f8251b6..fe5416b 100644 --- a/tests/lib/IPXWrapper/Tool/IPXRecv.pm +++ b/tests/lib/IPXWrapper/Tool/IPXRecv.pm @@ -40,7 +40,7 @@ sub new out => $out, in => $in, - sockets => {}, + sockets => [], }, $class); my $output = ""; @@ -53,11 +53,13 @@ sub new if($line =~ m{^Bound socket (\d+) to local address: (.+)/(.+)/(.+)$}) { - $self->{sockets}->{$1} = { + push(@{ $self->{sockets} }, { + fd => $1, + net => $2, node => $3, sock => $4, - }; + }); } elsif($line eq "Ready") { @@ -68,6 +70,34 @@ sub new die("Didn't get expected output from ipx-recv.exe:\n$output"); } +sub net +{ + my ($self, $sock_idx) = @_; + + if((scalar @{ $self->{sockets} }) > 1 && !defined($sock_idx)) + { + confess("IPXWrapper::Tool::IPXRecv::net() called without a \$sock_idx but multiple sockets are bound"); + } + + $sock_idx //= 0; + + return $self->{sockets}->[$sock_idx]->{net}; +} + +sub node +{ + my ($self, $sock_idx) = @_; + + if((scalar @{ $self->{sockets} }) > 1 && !defined($sock_idx)) + { + confess("IPXWrapper::Tool::IPXRecv::node() called without a \$sock_idx but multiple sockets are bound"); + } + + $sock_idx //= 0; + + return $self->{sockets}->[$sock_idx]->{node}; +} + sub DESTROY { my ($self) = @_; @@ -127,8 +157,10 @@ sub kill_and_read if($line =~ m{^Received (\d+) bytes \((.*)\) on socket (\d+) from (.+)/(.+)/(.+)$}) { + my ($sock) = grep { $_->{fd} == $3 } @{ $self->{sockets} }; + die("Received packet on unknown socket $3") - unless(defined($self->{sockets}->{$3})); + unless(defined $sock); die("Read a packet with the wrong size, binary data used in test?") unless($1 == length($2)); @@ -137,9 +169,9 @@ sub kill_and_read sock => $3, data => $2, - local_net => $self->{sockets}->{$3}->{net}, - local_node => $self->{sockets}->{$3}->{node}, - local_sock => $self->{sockets}->{$3}->{sock}, + local_net => $sock->{net}, + local_node => $sock->{node}, + local_sock => $sock->{sock}, src_network => $4, src_node => $5, diff --git a/tests/lib/IPXWrapper/Util.pm b/tests/lib/IPXWrapper/Util.pm index b4323f4..6224b8a 100644 --- a/tests/lib/IPXWrapper/Util.pm +++ b/tests/lib/IPXWrapper/Util.pm @@ -21,11 +21,18 @@ package IPXWrapper::Util; use Exporter qw(import); +use constant { + ENCAP_TYPE_DOSBOX => 2, +}; + our @EXPORT = qw( + ENCAP_TYPE_DOSBOX + run_remote_cmd reg_set_dword reg_set_addr + reg_set_string reg_delete_key reg_delete_value @@ -33,6 +40,7 @@ our @EXPORT = qw( send_ipx_packet_ethernet send_ipx_packet_novell send_ipx_packet_llc + send_ipx_packet_rfc1234 cmp_hashes_partial @@ -81,6 +89,13 @@ sub reg_set_addr run_remote_cmd($host_ip, "REG", "ADD", $key, "/v", $value, "/t", "REG_BINARY", "/d", $data, "/f"); } +sub reg_set_string +{ + my ($host_ip, $key, $value, $data) = @_; + + run_remote_cmd($host_ip, "REG", "ADD", $key, "/v", $value, "/t", "REG_SZ", "/d", $data, "/f"); +} + sub reg_delete_key { my ($host_ip, $key) = @_; @@ -176,6 +191,23 @@ sub send_ipx_packet_llc $enc_packet); } +sub send_ipx_packet_rfc1234 +{ + my (%options) = @_; + + my $packet = NetPacket::IPX->new(%options); + my $enc_packet = $packet->encode(); + + my $sock = IO::Socket::INET->new( + Proto => "udp", + PeerAddr => $options{dest_ip}, + PeerPort => $options{dest_port}, + ) or die("Can't create socket: $!"); + + $sock->send($enc_packet) + or die("Can't send data: $!"); +} + sub cmp_hashes_partial { my ($got, $expect) = @_;