File Coverage

blib/lib/Net/Address/Ethernet.pm
Criterion Covered Total %
statement 76 144 52.7
branch 10 48 20.8
condition 5 26 19.2
subroutine 19 20 95.0
pod 4 4 100.0
total 114 242 47.1


line stmt bran cond sub pod time code
1              
2             package Net::Address::Ethernet;
3              
4 5     5   69467 use warnings;
  5         14  
  5         164  
5 5     5   25 use strict;
  5         22  
  5         111  
6              
7             =head1 NAME
8              
9             Net::Address::Ethernet - find hardware ethernet address
10              
11             =head1 SYNOPSIS
12              
13             use Net::Address::Ethernet qw( get_address );
14             my $sAddress = get_address;
15              
16             =head1 FUNCTIONS
17              
18             The following functions will be exported to your namespace if you request :all like so:
19              
20             use Net::Address::Ethernet qw( :all );
21              
22             =over
23              
24             =cut
25              
26 5     5   21 use Carp;
  5         13  
  5         326  
27 5     5   2398 use Data::Dumper; # for debugging only
  5         32404  
  5         326  
28 5     5   36 use Exporter;
  5         11  
  5         135  
29 5     5   1916 use Net::Domain;
  5         37365  
  5         232  
30 5     5   2413 use Net::Ifconfig::Wrapper qw( Ifconfig );
  5         53492  
  5         322  
31 5     5   2218 use Regexp::Common;
  5         9618  
  5         25  
32 5     5   676722 use Sys::Hostname;
  5         4462  
  5         250  
33              
34 5     5   46 use constant DEBUG_MATCH => 0;
  5         13  
  5         372  
35              
36 5     5   29 use vars qw( $DEBUG $VERSION @EXPORT_OK %EXPORT_TAGS );
  5         38  
  5         329  
37 5     5   30 use base 'Exporter';
  5         10  
  5         754  
38              
39             $VERSION = 1.125;
40              
41             $DEBUG = 0 || $ENV{N_A_E_DEBUG};
42              
43             %EXPORT_TAGS = ( 'all' => [ qw( get_address get_addresses canonical is_address ), ], );
44             @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
45              
46             my @ahInfo;
47              
48             =item get_address
49              
50             Returns the 6-byte ethernet address in canonical form.
51             For example, '1A:2B:3C:4D:5E:6F'.
52              
53             When called in array context, returns a 6-element list representing
54             the 6 bytes of the address in decimal. For example,
55             (26,43,60,77,94,111).
56              
57             If any non-zero argument is given,
58             debugging information will be printed to STDERR.
59              
60             =cut
61              
62             sub get_address
63             {
64 4     4 1 34 if (0)
65             {
66             # If you know the name of the adapter, you can use this code to
67             # get its IP address:
68 5     5   32 use Socket qw/PF_INET SOCK_DGRAM inet_ntoa sockaddr_in/;
  5         15  
  5         1000  
69             if (! socket(SOCKET, PF_INET, SOCK_DGRAM, getprotobyname('ip')))
70             {
71             warn " WWW socket() failed\n";
72             goto IFCONFIG_VERSION;
73             } # if
74             # use ioctl() interface with SIOCGIFADDR.
75             my $ifreq = pack('a32', 'enp1s0');
76             if (! ioctl(SOCKET, 0x8915, $ifreq))
77             {
78             warn " WWW ioctl failed\n";
79             goto IFCONFIG_VERSION;
80             } # if
81             # Format the IP address from the output of ioctl().
82             my $s = inet_ntoa((sockaddr_in((unpack('a16 a16', $ifreq))[1]))[1]);
83             if (! $s)
84             {
85             warn " WWW inet_ntoa failed\n";
86             goto IFCONFIG_VERSION;
87             } # if
88 5     5   36 use Data::Dumper;
  5         12  
  5         6188  
89             warn Dumper($s); exit 88; # for debugging
90             } # if 0
91             IFCONFIG_VERSION:
92 4         19 my @a = get_addresses(@_);
93 4         23 _debug(" DDD in get_address, a is ", Dumper(\@a));
94             # Even if none are active, we'll return the first one:
95 4         20 my $sAddr = $a[0]->{sEthernet};
96             # Look through the list, returning the first active one that has a
97             # non-loopback IP address assigned to it:
98             TRY_ADDR:
99 4         20 foreach my $rh (@a)
100             {
101 4         13 my $sName = $rh->{sAdapter};
102 4         223 _debug(" DDD inspecting interface $sName...\n");
103 4 50       23 if (! $rh->{iActive})
104             {
105 4         17 _debug(" DDD but it is not active.\n");
106 4         15 next TRY_ADDR;
107             } # if
108 0         0 _debug(" DDD it is active...\n");
109 0 0       0 if (! exists $rh->{sIP})
110             {
111 0         0 _debug(" DDD but it has no IP address.\n");
112 0         0 next TRY_ADDR;
113             } # if
114 0 0       0 if (! defined $rh->{sIP})
115             {
116 0         0 _debug(" DDD but its IP address is undefined.\n");
117 0         0 next TRY_ADDR;
118             } # if
119 0 0       0 if ($rh->{sIP} eq '')
120             {
121 0         0 _debug(" DDD but its IP address is empty.\n");
122 0         0 next TRY_ADDR;
123             } # if
124 0 0       0 if ($rh->{sIP} eq '127.0.0.1')
125             {
126 0         0 _debug(" DDD but it's the loopback.\n");
127 0         0 next TRY_ADDR;
128             } # if
129 0 0       0 if (! exists $rh->{sEthernet})
130             {
131 0         0 _debug(" DDD but it has no ethernet address.\n");
132 0         0 next TRY_ADDR;
133             } # if
134 0 0       0 if (! defined $rh->{sEthernet})
135             {
136 0         0 _debug(" DDD but its ethernet address is undefined.\n");
137 0         0 next TRY_ADDR;
138             } # if
139 0 0       0 if ($rh->{sEthernet} eq q{})
140             {
141 0         0 _debug(" DDD but its ethernet address is empty.\n");
142 0         0 next TRY_ADDR;
143             } # if
144 0         0 $sAddr = $rh->{sEthernet};
145 0         0 _debug(" DDD and its ethernet address is $sAddr.\n");
146 0         0 last TRY_ADDR;
147             } # foreach TRY_ADDR
148 4 100       86 return wantarray ? map { hex } split(/[-:]/, $sAddr) : $sAddr;
  0         0  
149             } # get_address
150              
151              
152             =item get_addresses
153              
154             Returns an array of hashrefs.
155             Each hashref describes one Ethernet adapter found in the current hardware configuration,
156             with the following entries filled in to the best of our ability to determine:
157              
158             =over
159              
160             =item sEthernet -- The MAC address in canonical form.
161              
162             =item rasIP -- A reference to an array of all the IP addresses on this adapter.
163              
164             =item sIP -- The "first" IP address on this adapter.
165              
166             =item sAdapter -- The name of this adapter.
167              
168             =item iActive -- Whether this adapter is active.
169              
170             =back
171              
172             For example:
173              
174             {
175             'sAdapter' => 'Ethernet adapter Local Area Connection',
176             'sEthernet' => '12:34:56:78:9A:BC',
177             'rasIP' => ['111.222.33.44',],
178             'sIP' => '111.222.33.44',
179             'iActive' => 1,
180             },
181              
182             If any non-zero argument is given,
183             debugging information will be printed to STDERR.
184              
185             =cut
186              
187             sub get_addresses
188             {
189 6   33 6 1 3063 $DEBUG ||= shift;
190             # Short-circuit if this function has already been called:
191 6 50 33     49 if (! $DEBUG && @ahInfo)
192             {
193 0         0 goto ALL_DONE;
194             } # if
195 6         17 my $sAddr = undef;
196 6         40 my $rh = Ifconfig('list', '', '', '');
197 6 50 33     17539 if (! defined $rh || (! scalar keys %$rh))
198             {
199 6         379 warn " EEE Ifconfig failed: $@";
200             } # if
201 6         88 _debug(" DDD raw output from Ifconfig is ", Dumper($rh));
202             # Convert their hashref to our array format:
203 6         48 foreach my $key (keys %$rh)
204             {
205 0         0 my %hash;
206 0         0 _debug(" DDD working on key $key...\n");
207 0         0 my $sAdapter = $key;
208 0 0       0 if ($key =~ m!\A\{.+}\z!)
209             {
210 0         0 $sAdapter = $rh->{$key}->{descr};
211             } # if
212 0         0 $hash{sAdapter} = $sAdapter;
213 0         0 my @asIP = keys %{$rh->{$key}->{inet}};
  0         0  
214             # Thanks to Sergey Kotenko for the array idea:
215 0         0 $hash{rasIP} = \@asIP;
216 0         0 $hash{sIP} = $asIP[0];
217 0   0     0 my $sEther = $rh->{$key}->{ether} || '';
218 0 0       0 if ($sEther eq '')
219             {
220 0         0 $sEther = _find_mac($sAdapter, $hash{sIP});
221             } # if
222 0         0 $hash{sEthernet} = canonical($sEther);
223 0         0 $hash{iActive} = 0;
224 0 0 0     0 if (defined $rh->{$key}->{status} && ($rh->{$key}->{status} =~ m!\A(1|UP)\z!i))
225             {
226 0         0 $hash{iActive} = 1;
227             } # if
228 0         0 push @ahInfo, \%hash;
229             } # foreach
230             ALL_DONE:
231 6         45 return @ahInfo;
232             } # get_addresses
233              
234              
235             # Attempt other ways of finding the MAC Address:
236             sub _find_mac
237             {
238 0   0 0   0 my $sAdapter = shift || return;
239 0   0     0 my $sIP = shift || '';
240             # No hope on some OSes:
241 0 0       0 return if ($^O eq 'MSWIn32');
242 0         0 my @asARP = qw( /usr/sbin/arp /sbin/arp /bin/arp /usr/bin/arp );
243 0   0     0 my $sHostname = hostname || Net::Domain::hostname || '';
244 0   0     0 my $sHostfqdn = Net::Domain::hostfqdn || '';
245 0         0 my @asHost = ($sHostname, $sHostfqdn, '');
246             ARP:
247 0         0 foreach my $sARP (@asARP)
248             {
249 0 0       0 next ARP if ! -x $sARP;
250             HOSTNAME:
251 0         0 foreach my $sHost (@asHost)
252             {
253 0   0     0 $sHost ||= q{};
254 0 0       0 next HOSTNAME if ($sHost eq q{});
255 0         0 my $sCmd = qq{$sARP $sHost};
256             # print STDERR " DDD trying ==$sCmd==\n";
257 0         0 my @as = qx{$sCmd};
258             LINE_OF_CMD:
259 0         0 while (@as)
260             {
261 0         0 my $sLine = shift @as;
262 0         0 DEBUG_MATCH && print STDERR " DDD output line of cmd ==$sLine==\n";
263 0 0       0 if ($sLine =~ m!\(($RE{net}{IPv4})\)\s+AT\s+($RE{net}{MAC})\b!i)
264             {
265             # Looks like arp on Solaris.
266 0         0 my ($sIPFound, $sEtherFound) = ($1, $2);
267             # print STDERR " DDD found IP =$sIPFound=, found ether =$sEtherFound=\n";
268 0 0       0 return $sEtherFound if ($sIPFound eq $sIP);
269             # print STDERR " DDD does NOT match the one I wanted =$sIP=\n";
270             } # if
271 0 0       0 if ($sLine =~ m!($RE{net}{IPv4})\s+ETHER\s+($RE{net}{MAC})\b!i)
272             {
273             # Looks like arp on Solaris.
274 0 0       0 return $2 if ($1 eq $sIP);
275             } # if
276             } # while LINE_OF_CMD
277             } # foreach HOSTNAME
278             } # foreach ARP
279             } # _find_mac
280              
281             =item is_address
282              
283             Returns a true value if its argument looks like an ethernet address.
284              
285             =cut
286              
287             sub is_address
288             {
289 20   100 20 1 8680 my $s = uc(shift || '');
290             # Convert all non-hex digits to colon:
291 20         114 $s =~ s![^0-9A-F]+!:!g;
292 20         132 return ($s =~ m!\A$RE{net}{MAC}\Z!i);
293             } # is_address
294              
295              
296             =item canonical
297              
298             Given a 6-byte ethernet address, converts it to canonical form.
299             Canonical form is 2-digit uppercase hexadecimal numbers with colon
300             between the bytes. The address to be converted can have any kind of
301             punctuation between the bytes, the bytes can be 1-digit, and the bytes
302             can be lowercase; but the bytes must already be hex.
303              
304             =cut
305              
306             sub canonical
307             {
308 10     10 1 1785 my $s = shift;
309 10 100       23 return '' if ! is_address($s);
310             # Convert all non-hex digits to colon:
311 6         777 $s =~ s![^0-9a-fA-F]+!:!g;
312 6         30 my @as = split(':', $s);
313             # Cobble together 2-digit hex bytes:
314 6         12 $s = '';
315 6 100       14 map { $s .= length() < 2 ? "0$_" : $_; $s .= ':' } @as;
  36         76  
  36         66  
316 6         12 chop $s;
317 6         32 return uc $s;
318             } # canonical
319              
320             sub _debug
321             {
322 18 50   18   1086 return if ! $DEBUG;
323 0           print STDERR @_;
324             } # _debug
325              
326             =back
327              
328             =head1 NOTES
329              
330             =head1 SEE ALSO
331              
332             arp, ifconfig, ipconfig
333              
334             =head1 BUGS
335              
336             Please tell the author if you find any! And please show me the output
337             of `arp `
338             or `ifconfig`
339             or `ifconfig -a`
340             from your system.
341              
342             =head1 AUTHOR
343              
344             Martin 'Kingpin' Thurn, C, L.
345              
346             =head1 LICENSE
347              
348             This software is released under the same license as Perl itself.
349              
350             =cut
351              
352             1;
353              
354             __END__