File Coverage

blib/lib/Net/Address/Ethernet.pm
Criterion Covered Total %
statement 124 144 86.1
branch 30 48 62.5
condition 15 26 57.6
subroutine 20 20 100.0
pod 4 4 100.0
total 193 242 79.7


line stmt bran cond sub pod time code
1              
2             package Net::Address::Ethernet;
3              
4 5     5   66316 use warnings;
  5         9  
  5         137  
5 5     5   16 use strict;
  5         5  
  5         91  
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   16 use Carp;
  5         10  
  5         296  
27 5     5   2516 use Data::Dumper; # for debugging only
  5         33302  
  5         345  
28 5     5   33 use Exporter;
  5         8  
  5         139  
29 5     5   2072 use Net::Domain;
  5         35008  
  5         225  
30 5     5   2531 use Net::Ifconfig::Wrapper qw( Ifconfig );
  5         51869  
  5         372  
31 5     5   2806 use Regexp::Common;
  5         9331  
  5         27  
32 5     5   558572 use Sys::Hostname;
  5         4342  
  5         292  
33              
34 5     5   56 use constant DEBUG_MATCH => 0;
  5         4  
  5         331  
35              
36 5     5   18 use vars qw( $DEBUG $VERSION @EXPORT_OK %EXPORT_TAGS );
  5         6  
  5         273  
37 5     5   18 use base 'Exporter';
  5         5  
  5         725  
38              
39             $VERSION = 1.124;
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 23 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   19 use Socket qw/PF_INET SOCK_DGRAM inet_ntoa sockaddr_in/;
  5         7  
  5         891  
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         5  
  5         5673  
89             warn Dumper($s); exit 88; # for debugging
90             } # if 0
91             IFCONFIG_VERSION:
92 4         16 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         36 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         13 foreach my $rh (@a)
100             {
101 7         10 my $sName = $rh->{sAdapter};
102 7         19 _debug(" DDD inspecting interface $sName...\n");
103 7 50       16 if (! $rh->{iActive})
104             {
105 0         0 _debug(" DDD but it is not active.\n");
106 0         0 next TRY_ADDR;
107             } # if
108 7         11 _debug(" DDD it is active...\n");
109 7 50       13 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 7 50       20 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 7 50       12 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 7 100       24 if ($rh->{sIP} eq '127.0.0.1')
125             {
126 3         5 _debug(" DDD but it's the loopback.\n");
127 3         5 next TRY_ADDR;
128             } # if
129 4 50       11 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       12 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       34 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         8 $sAddr = $rh->{sEthernet};
145 4         13 _debug(" DDD and its ethernet address is $sAddr.\n");
146 4         8 last TRY_ADDR;
147             } # foreach TRY_ADDR
148 4 100       29 return wantarray ? map { hex } split(/[-:]/, $sAddr) : $sAddr;
  6         10  
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 1260 $DEBUG ||= shift;
190             # Short-circuit if this function has already been called:
191 6 100 66     41 if (! $DEBUG && @ahInfo)
192             {
193 3         11 goto ALL_DONE;
194             } # if
195 3         6 my $sAddr = undef;
196 3         19 my $rh = Ifconfig('list', '', '', '');
197 3 50 33     3497644 if (! defined $rh || (! scalar keys %$rh))
198             {
199 0         0 warn " EEE Ifconfig failed: $@";
200             } # if
201 3         34 _debug(" DDD raw output from Ifconfig is ", Dumper($rh));
202             # Convert their hashref to our array format:
203 3         129 foreach my $key (keys %$rh)
204             {
205 6         12 my %hash;
206 6         25 _debug(" DDD working on key $key...\n");
207 6         8 my $sAdapter = $key;
208 6 50       23 if ($key =~ m!\A\{.+}\z!)
209             {
210 0         0 $sAdapter = $rh->{$key}->{descr};
211             } # if
212 6         28 $hash{sAdapter} = $sAdapter;
213 6         8 my @asIP = keys %{$rh->{$key}->{inet}};
  6         27  
214             # Thanks to Sergey Kotenko for the array idea:
215 6         19 $hash{rasIP} = \@asIP;
216 6         14 $hash{sIP} = $asIP[0];
217 6   100     36 my $sEther = $rh->{$key}->{ether} || '';
218 6 100       18 if ($sEther eq '')
219             {
220 3         18 $sEther = _find_mac($sAdapter, $hash{sIP});
221             } # if
222 6         35 $hash{sEthernet} = canonical($sEther);
223 6         595 $hash{iActive} = 0;
224 6 50 33     79 if (defined $rh->{$key}->{status} && ($rh->{$key}->{status} =~ m!\A(1|UP)\z!i))
225             {
226 6         24 $hash{iActive} = 1;
227             } # if
228 6         21 push @ahInfo, \%hash;
229             } # foreach
230             ALL_DONE:
231 6         44 return @ahInfo;
232             } # get_addresses
233              
234              
235             # Attempt other ways of finding the MAC Address:
236             sub _find_mac
237             {
238 3   50 3   12 my $sAdapter = shift || return;
239 3   50     10 my $sIP = shift || '';
240             # No hope on some OSes:
241 3 50       20 return if ($^O eq 'MSWIn32');
242 3         17 my @asARP = qw( /usr/sbin/arp /sbin/arp /bin/arp /usr/bin/arp );
243 3   50     23 my $sHostname = hostname || Net::Domain::hostname || '';
244 3   50     58 my $sHostfqdn = Net::Domain::hostfqdn || '';
245 3         1278 my @asHost = ($sHostname, $sHostfqdn, '');
246             ARP:
247 3         9 foreach my $sARP (@asARP)
248             {
249 12 100       278 next ARP if ! -x $sARP;
250             HOSTNAME:
251 3         7 foreach my $sHost (@asHost)
252             {
253 9   100     670 $sHost ||= q{};
254 9 100       34 next HOSTNAME if ($sHost eq q{});
255 6         15 my $sCmd = qq{$sARP $sHost};
256             # print STDERR " DDD trying ==$sCmd==\n";
257 6         3346077 my @as = qx{$sCmd};
258             LINE_OF_CMD:
259 6         97 while (@as)
260             {
261 3         16 my $sLine = shift @as;
262 3         7 DEBUG_MATCH && print STDERR " DDD output line of cmd ==$sLine==\n";
263 3 50       77 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 3 50       1299 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 26   100 26 1 4658 my $s = uc(shift || '');
290             # Convert all non-hex digits to colon:
291 26         116 $s =~ s![^0-9A-F]+!:!g;
292 26         163 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 16     16 1 687 my $s = shift;
309 16 100       31 return '' if ! is_address($s);
310             # Convert all non-hex digits to colon:
311 10         1077 $s =~ s![^0-9a-fA-F]+!:!g;
312 10         34 my @as = split(':', $s);
313             # Cobble together 2-digit hex bytes:
314 10         13 $s = '';
315 10 100       16 map { $s .= length() < 2 ? "0$_" : $_; $s .= ':' } @as;
  60         79  
  60         67  
316 10         16 chop $s;
317 10         37 return uc $s;
318             } # canonical
319              
320             sub _debug
321             {
322 34 50   34   1025 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__