File Coverage

blib/lib/Net/Address/Ethernet.pm
Criterion Covered Total %
statement 108 166 65.0
branch 30 62 48.3
condition 6 26 23.0
subroutine 18 19 94.7
pod 4 4 100.0
total 166 277 59.9


line stmt bran cond sub pod time code
1              
2             package Net::Address::Ethernet;
3              
4 4     4   417070 use warnings;
  4         40  
  4         135  
5 4     4   23 use strict;
  4         8  
  4         95  
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 4     4   21 use Carp;
  4         8  
  4         211  
27 4     4   2585 use Data::Dumper; # for debugging only
  4         28487  
  4         249  
28 4     4   30 use Exporter;
  4         8  
  4         135  
29 4     4   1849 use Net::Domain;
  4         35215  
  4         228  
30 4     4   2336 use Net::Ifconfig::Wrapper qw( Ifconfig );
  4         854588  
  4         331  
31 4     4   2828 use Regexp::Common;
  4         11288  
  4         27  
32 4     4   650251 use Sys::Hostname;
  4         4571  
  4         260  
33              
34 4     4   43 use constant DEBUG_MATCH => 0;
  4         8  
  4         333  
35              
36 4     4   28 use vars qw( $DEBUG $VERSION @EXPORT_OK %EXPORT_TAGS );
  4         8  
  4         269  
37 4     4   26 use base 'Exporter';
  4         10  
  4         882  
38              
39             $VERSION = 1.128;
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             # warn " TTT get_address()";
65 4     4 1 349 if (0)
66             {
67             # If you know the name of the adapter, you can use this code to
68             # get its IP address:
69 4     4   36 use Socket qw/PF_INET SOCK_DGRAM inet_ntoa sockaddr_in/;
  4         11  
  4         8775  
70             if (! socket(SOCKET, PF_INET, SOCK_DGRAM, getprotobyname('ip')))
71             {
72             warn " WWW socket() failed\n";
73             goto IFCONFIG_VERSION;
74             } # if
75             # use ioctl() interface with SIOCGIFADDR.
76             my $ifreq = pack('a32', 'enp1s0');
77             if (! ioctl(SOCKET, 0x8915, $ifreq))
78             {
79             warn " WWW ioctl failed\n";
80             goto IFCONFIG_VERSION;
81             } # if
82             # Format the IP address from the output of ioctl().
83             my $s = inet_ntoa((sockaddr_in((unpack('a16 a16', $ifreq))[1]))[1]);
84             if (! $s)
85             {
86             warn " WWW inet_ntoa failed\n";
87             goto IFCONFIG_VERSION;
88             } # if
89             warn Dumper($s); exit 88; # for debugging
90             } # if 0
91             IFCONFIG_VERSION:
92 4         22 my @a = get_addresses(@_);
93 4         27 _debug(" DDD in get_address, a is ", Dumper(\@a));
94             # Even if none are active, we'll return the first one:
95 4         22 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         28 foreach my $rh (@a)
100             {
101 8         26 my $sName = $rh->{sAdapter};
102 8         33 _debug(" DDD inspecting interface $sName...\n");
103 8 50       30 if (! $rh->{iActive})
104             {
105 0         0 _debug(" DDD but it is not active.\n");
106 0         0 next TRY_ADDR;
107             } # if
108 8         28 _debug(" DDD it is active...\n");
109 8 50       22 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 8 50       26 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 8 50       29 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 8 100       34 if ($rh->{sIP} eq '127.0.0.1')
125             {
126 4         13 _debug(" DDD but it's the loopback.\n");
127 4         12 next TRY_ADDR;
128             } # if
129 4 50       24 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 4 50       25 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 4 50       15 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 4         9 $sAddr = $rh->{sEthernet};
145 4         38 _debug(" DDD and its ethernet address is $sAddr.\n");
146 4         11 last TRY_ADDR;
147             } # foreach TRY_ADDR
148 4 100       69 return wantarray ? map { hex } split(/[-:]/, $sAddr) : $sAddr;
  6         29  
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             # warn " TTT get_addresses()";
190 6   33 6 1 2863 $DEBUG ||= shift;
191             # Short-circuit if this function has already been called:
192 6 100 66     43 if (! $DEBUG && @ahInfo)
193             {
194 3         13 goto ALL_DONE;
195             } # if
196 3         9 my $sAddr = undef;
197 3         17 my $rh = Ifconfig('list', '', '', '');
198 3 50 33     14218 if ((! defined $rh) || (! scalar keys %$rh))
199             {
200             # warn " WWW Ifconfig failed: $@";
201 3 50       99 if ($@ =~ m/not found/)
202             {
203             # At this point we might try another method, such as calling /sbin/ip
204 3         47 my $sCmdIp = '/sbin/ip';
205 3 50       168 if (! -f $sCmdIp)
206             {
207 0         0 warn " DDD $sCmdIp does not exist";
208             }
209             else
210             {
211 3         47 $sCmdIp .= q/ addr show/;
212 3         11531 my @asOutput = qx/$sCmdIp/;
213             # print STDERR " DDD asOutput ==@asOutput";
214 3         101 my $sInterface = q//;
215 3         49 my %hash;
216 3         66 foreach my $sLine (@asOutput)
217             {
218             # print STDERR " DDD sLine ==$sLine";
219 24 100       247 if ($sLine =~ m/\d:\s(.+?):.+,UP/)
220             {
221             # Found an interface that is in UP state
222 6 100       99 push @ahInfo, {%hash} if %hash;
223 6         94 $sInterface = $1;
224             # Start a new adapter's info:
225 6         35 %hash = ();
226 6         55 $hash{sAdapter} = $sInterface;
227 6         36 $hash{iActive} = 1;
228 6         114 _debug(" DDD hash is ", Dumper(\%hash));
229             } # if
230 24 100       117 if ($sLine =~ m/ether\s+(([0-9a-f]{2}:){5}[0-9a-f]{2})/)
231             {
232 3         42 $hash{sEthernet} = canonical($1);
233 3         29 _debug(" DDD hash is ", Dumper(\%hash));
234             } # if
235 24 100       146 if ($sLine =~ m/inet\s+((\d+\.){3}\d+)/)
236             {
237 6         38 $hash{sAdapter} = $sInterface;
238 6         54 $hash{sIP} = $1;
239 6         44 $hash{rasIP} = [$1];
240 6         41 _debug(" DDD hash is ", Dumper(\%hash));
241             } # if
242             } # foreach
243 3 50       45 push @ahInfo, {%hash} if %hash;
244             } # if
245             } # if
246             # No sense trying to parse non-existent output:
247 3         57 goto ALL_DONE;
248             } # if
249 0         0 _debug(" DDD raw output from Ifconfig is ", Dumper($rh));
250             # Convert their hashref to our array format:
251 0         0 foreach my $key (keys %$rh)
252             {
253 0         0 my %hash;
254 0         0 _debug(" DDD working on key $key...\n");
255 0         0 my $sAdapter = $key;
256 0 0       0 if ($key =~ m!\A\{.+}\z!)
257             {
258 0         0 $sAdapter = $rh->{$key}->{descr};
259             } # if
260 0         0 $hash{sAdapter} = $sAdapter;
261 0         0 my @asIP = keys %{$rh->{$key}->{inet}};
  0         0  
262             # Thanks to Sergey Kotenko for the array idea:
263 0         0 $hash{rasIP} = \@asIP;
264 0         0 $hash{sIP} = $asIP[0];
265 0   0     0 my $sEther = $rh->{$key}->{ether} || '';
266 0 0       0 if ($sEther eq '')
267             {
268 0         0 $sEther = _find_mac($sAdapter, $hash{sIP});
269             } # if
270 0         0 $hash{sEthernet} = canonical($sEther);
271 0         0 $hash{iActive} = 0;
272 0 0 0     0 if (defined $rh->{$key}->{status} && ($rh->{$key}->{status} =~ m!\A(1|UP)\z!i))
273             {
274 0         0 $hash{iActive} = 1;
275             } # if
276 0         0 push @ahInfo, \%hash;
277             } # foreach
278             ALL_DONE:
279 6         41 return @ahInfo;
280             } # get_addresses
281              
282              
283             # Attempt other ways of finding the MAC Address:
284             sub _find_mac
285             {
286 0   0 0   0 my $sAdapter = shift || return;
287 0   0     0 my $sIP = shift || '';
288             # No hope on some OSes:
289 0 0       0 return if ($^O eq 'MSWIn32');
290 0         0 my @asARP = qw( /usr/sbin/arp /sbin/arp /bin/arp /usr/bin/arp );
291 0   0     0 my $sHostname = hostname || Net::Domain::hostname || '';
292 0   0     0 my $sHostfqdn = Net::Domain::hostfqdn || '';
293 0         0 my @asHost = ($sHostname, $sHostfqdn, '');
294             ARP:
295 0         0 foreach my $sARP (@asARP)
296             {
297 0 0       0 next ARP if ! -x $sARP;
298             HOSTNAME:
299 0         0 foreach my $sHost (@asHost)
300             {
301 0   0     0 $sHost ||= q{};
302 0 0       0 next HOSTNAME if ($sHost eq q{});
303 0         0 my $sCmd = qq{$sARP $sHost};
304             # print STDERR " DDD trying ==$sCmd==\n";
305 0         0 my @as = qx{$sCmd};
306             LINE_OF_CMD:
307 0         0 while (@as)
308             {
309 0         0 my $sLine = shift @as;
310 0         0 DEBUG_MATCH && print STDERR " DDD output line of cmd ==$sLine==\n";
311 0 0       0 if ($sLine =~ m!\(($RE{net}{IPv4})\)\s+AT\s+($RE{net}{MAC})\b!i)
312             {
313             # Looks like arp on Solaris.
314 0         0 my ($sIPFound, $sEtherFound) = ($1, $2);
315             # print STDERR " DDD found IP =$sIPFound=, found ether =$sEtherFound=\n";
316 0 0       0 return $sEtherFound if ($sIPFound eq $sIP);
317             # print STDERR " DDD does NOT match the one I wanted =$sIP=\n";
318             } # if
319 0 0       0 if ($sLine =~ m!($RE{net}{IPv4})\s+ETHER\s+($RE{net}{MAC})\b!i)
320             {
321             # Looks like arp on Solaris.
322 0 0       0 return $2 if ($1 eq $sIP);
323             } # if
324             } # while LINE_OF_CMD
325             } # foreach HOSTNAME
326             } # foreach ARP
327             } # _find_mac
328              
329             =item is_address
330              
331             Returns a true value if its argument looks like an ethernet address.
332              
333             =cut
334              
335             sub is_address
336             {
337 23   100 23 1 6638 my $s = uc(shift || '');
338             # Convert all non-hex digits to colon:
339 23         189 $s =~ s![^0-9A-F]+!:!g;
340 23         252 return ($s =~ m!\A$RE{net}{MAC}\Z!i);
341             } # is_address
342              
343              
344             =item canonical
345              
346             Given a 6-byte ethernet address, converts it to canonical form.
347             Canonical form is 2-digit uppercase hexadecimal numbers with colon
348             between the bytes. The address to be converted can have any kind of
349             punctuation between the bytes, the bytes can be 1-digit, and the bytes
350             can be lowercase; but the bytes must already be hex.
351              
352             =cut
353              
354             sub canonical
355             {
356 13     13 1 904 my $s = shift;
357 13 100       52 return '' if ! is_address($s);
358             # Convert all non-hex digits to colon:
359 10         2905 $s =~ s![^0-9a-fA-F]+!:!g;
360 10         63 my @as = split(':', $s);
361             # Cobble together 2-digit hex bytes:
362 10         30 $s = '';
363 10 100       27 map { $s .= length() < 2 ? "0$_" : $_; $s .= ':' } @as;
  60         133  
  60         118  
364 10         39 chop $s;
365 10         64 return uc $s;
366             } # canonical
367              
368             sub _debug
369             {
370 43 50   43   2194 return if ! $DEBUG;
371 0           print STDERR @_;
372             } # _debug
373              
374             =back
375              
376             =head1 NOTES
377              
378             =head1 SEE ALSO
379              
380             arp, ifconfig, ipconfig
381              
382             =head1 BUGS
383              
384             Please tell the author if you find any! And please show me the output
385             of `arp `
386             or `ifconfig`
387             or `ifconfig -a`
388             from your system.
389              
390             =head1 AUTHOR
391              
392             Martin 'Kingpin' Thurn, C, L.
393              
394             =head1 LICENSE
395              
396             This software is released under the same license as Perl itself.
397              
398             =cut
399              
400             1;
401              
402             __END__