pear2/Net_RouterOS

Getting user MAC address resolves in error

brsnik opened this issue · 27 comments

We are using this script that we found in the Wiki page:

$client = new RouterOS\Client($ip, 'name', 'name');
        $printRequest = new RouterOS\Request('/ip arp print .proplist=mac-address');
            $printRequest->setQuery(
                RouterOS\Query::where('address', $_SERVER['REMOTE_ADDR'])
            );
    $mac = $client->sendSync($printRequest)->getProperty('mac-address');

    if (null !== $mac) {
        echo 'CONNECTED: '.$ip.'<br>MAC: ', $mac;
    } else {
        echo 'Your IP ('.$ip.$mac.
        ') is not part of our network, and because of that, we can\'t determine your MAC address';
    }

However, we get the following error:
Warning: call_user_func_array() expects parameter 1 to be a valid callback, class 'PEAR2\Net\RouterOS\Response' does not have a method 'getProperty' in/home/admin/web/domain/public_html/PEAR2/Net/RouterOS/ResponseCollection.php on line 356

Has there been some kind of an update, and we are just using an older version of the script?

So far, we are able to establish a connection.

Thanks.

Response::getProperty() exists since the latest released version, i.e. 1.0.0b5. If you're using an older version, then yes, you're not going to have it.

But I'm guessing you only "just" downloaded it, given your previous issue, so I don't see how could this be... I'll double check the example and the client (not today though...), but in the meantime, one possible workaround might be to first get the first response explicitly with

$client->sendSync($printRequest)->current()->getProperty('mac-address')

or if even that doesn't work, see what methods you do have with:

var_dump(get_class_methods($client->sendSync($printRequest)->current()));

When using
$mac = $client->sendSync($printRequest)->current()->getProperty('mac-address');

I got this error:
PEAR2\Net\Transmitter\SocketException: Failed to connect with socket. in /home/admin/web/domain/public_html/PEAR2/Net/Transmitter/TcpClient.php:199 Stack trace: #0 /home/admin/web/domain/public_html/PEAR2/Net/Transmitter/TcpClient.php(160): PEAR2\Net\Transmitter\TcpClient->createException('Failed to cone...', 8) #1 /home/admin/web/domain/public_html/PEAR2/Net/RouterOS/Communicator.php(141): PEAR2\Net\Transmitter\TcpClient->__construct('95.xx.xx.81', 8728, false, '60', 'uniroyal%2Funir...', '', Resource id #1) #2 /home/admin/web/domain/public_html/PEAR2/Net/RouterOS/Client.php(139): PEAR2\Net\RouterOS\Communicator->__construct('95.xx.xx.81', 8728, false, NULL, 'uniroyal/uniroy...', '', NULL) #3 /home/admin/web/domain/public_html/mtk_api.php(30): PEAR2\Net\RouterOS\Client->__construct('95.xx.xx.81', 'uniroyal', 'uniroyal') #4 {main} Next PEAR2\Net\RouterOS\SocketException: Error connecting to RouterOS in /home/admin/web/domain/public_html/PEAR2/Net/RouterOS/Communicator.php:151 Stack trace: #0 /home/admin/web/domain/public_html/PEAR2/Net/RouterOS/Client.php(139): PEAR2\Net\RouterOS\Communicator->__construct('95.xx.xx.81', 8728, false, NULL, 'uniroyal/uniroy...', '', NULL) #1 /home/admin/web/domain/public_html/mtk_api.php(30): PEAR2\Net\RouterOS\Client->__construct('95.xx.xx.81', 'uniroyal', 'uniroyal') #2 {main}

And with
$mac = var_dump(get_class_methods($client->sendSync($printRequest)->current()));

I got this error:

Fatal error: Uncaught Error: Call to undefined method PEAR2\Net\RouterOS\Response::getProperty() in /home/admin/web/domain/public_html/mtk_api.php:42 Stack trace: #0 {main} thrown in/home/admin/web/domain/public_html/mtk_api.php on line 42

array(9) { [0]=> string(11) "__construct" [1]=> string(7) "getType" [2]=> string(20) "getUnrecognizedWords" [3]=> string(8) "__invoke" [4]=> string(20) "sanitizeArgumentName" [5]=> string(21) "sanitizeArgumentValue" [6]=> string(6) "getTag" [7]=> string(11) "getArgument" [8]=> string(15) "getAllArguments" } 

Yeah, you're definitely using an older version. I highly recommend you upgrade to the latest released version (see the releases page).

I've downloaded the latest version 1.0.0b5, however it still doesn't seem to work.

With: $mac = var_dump(get_class_methods($client->sendSync($printRequest)->current()));

I get: array(11) { [0]=> string(11) "__construct" [1]=> string(7) "getType" [2]=> string(11) "getArgument" [3]=> string(11) "getProperty" [4]=> string(20) "getUnrecognizedWords" [5]=> string(5) "count" [6]=> string(8) "__invoke" [7]=> string(21) "sanitizeAttributeName" [8]=> string(22) "sanitizeAttributeValue" [9]=> string(6) "getTag" [10]=> string(11) "getIterator" }

The other two:
$mac = $client->sendSync($printRequest)->current()->getProperty('mac-address');
and $mac = $client->sendSync($printRequest)->getProperty('mac-address'); just returns null.

false? Are you sure it's not null?

null is returned when the property isn't present in the response. In turn, the example code

if (null !== $mac) {

checks that, and shows the error message.

And as the message says, perhaps the IP you're accessing the web page with is not in the ARP table?

Do a

var_dump($_SERVER['REMOTE_ADDR']);

and see if that IP is present in the ARP table.

Its null, not false sorry.

Just in case there is something wrong with the condition, I am echoing $mac in else.

echo '<br>Your IP ('.$ip.$mac.
        ') is not part of our network, and because of that, we can\'t determine your MAC address';

I'll do a var_dump($_SERVER['REMOTE_ADDR']) and let you know.

In your modified code, $ip is the router IP, but what you want the error message to show is $_SERVER['REMOTE_ADDR'], because that is what the query checks for. That's the IP not present in the ARP table... although strictly speaking, I guess the router's IP is not in the ARP table either.

$ip = $_SERVER['REMOTE_ADDR']; I am using it this way in order to be able to have multiple routers access this page.

This way it does establish a connection with the router, with the connection test script.

I should try something like this then $ip = var_dump($_SERVER['REMOTE_ADDR']);?

In that case, yeah, the script will always be false, because the router's IP is not part of the ARP table.

You need to modify the line

RouterOS\Query::where('address', $_SERVER['REMOTE_ADDR'])

to use something other than $_SERVER['REMOTE_ADDR'].

So how do you propose I get the IP so I can have multiple routers opening this page, with the same login info just different IPs?

The IPs on some of these routers will be dynamic...so it is important that it does that.

What we need is to grab the Mac of the device that is accessing this page/script—therefore identify the user, so we can display that user some relevant info from a DB.

That depends on a lot of factors about the router configuration.

If the routers uses hotspot, and you have disabled anonymous proxy, you can look for the $_SERVER['X_FORWARDED_FOR'] header.

Another way, which works for almost every other scenario is to lookup the $_SERVER['REMOTE_PORT'] in the connection tracking menu, and from that item, take... it was either src-reply-address or dst-reply-address... as the IP address that you'd then lookup in the ARP table.

So either:

$printRequest->setQuery(
    RouterOS\Query::where('src-reply-address', $_SERVER['REMOTE_PORT'])
);

or this then:

$printRequest->setQuery(
    RouterOS\Query::where('dst-reply-address', $_SERVER['REMOTE_PORT'])
);

Or did you mean $_SERVER['X_FORWARDED_FOR'] instead of $_SERVER['REMOTE_PORT']?

Not quite that simple I'm afraid...

I had written something like that at one point, but since deleted it, as it turned out I could use $_SERVER['X_FORWARDED_FOR'] for that router... and if I've saved a copy, I can't find it now.

I could get back to you with working code in a few days, but rather than wait for me, I suggest you examine in detail the /ip firewall connection menu, and pay close attention to occurrences of $_SERVER['REMOTE_ADDR'] and $_SERVER['REMOTE_PORT']. You'll figure it out before me 😉 .

You mentioned something about hotspot above. It should "hopefully" work regardless whether its a hotspot or not.

I'll try, thought I doubt I'll solve it...a total newb with RouterOS and MikroTik in general.

If I don't post anything here, and if you manage to solve it please share...it would definitely be of great help as it's the core of my project.

Well... sorry for having taken so long...

I finally found a continious stretch of free time to fully test this out... And this is what I ended up with:

$util = new RouterOS\Util(
    $client = new RouterOS\Client($_SERVER['REMOTE_ADDR'], 'admin', 'password')
);

$ip = isset($_SERVER['X_FORWARDED_FOR']) ? $_SERVER['X_FORWARDED_FOR'] : strstr(
    $util->setMenu('/ip firewall connection')->get(
        $util->find(
            RouterOS\Query::where('protocol', 'tcp')
                ->andWhere('reply-dst-address', $_SERVER['REMOTE_ADDR'] . ':' . $_SERVER['REMOTE_PORT'])
                ->andWhere('reply-src-address', (isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : $_SERVER['LOCAL_ADDR']) . ':' . $_SERVER['SERVER_PORT'])
        ),
       'src-address'
    ),
    ':',
    true
);

$mac = $util->setMenu('/ip arp')->get($util->find(RouterOS\Query::where('address', $ip)), 'mac-address');

The key being the $ip more than anything else - that's the internal IP address of the device accessing the page. From there, it's effectively the same deal as before (though I wrote it using Util, because why not...).

Note that this only works for one level NAT (RouterOS has a public IP). A double level NAT won't work unless you setup the router and web server be inside a common VPN (so that they can see each other's IPs directly).

Hello @boenrobot ,
We have managed to setup a vpn network. Mikrotik and VPS server are all connected to VPN server. I can connect to mikrotik api but i can't find a way to get the local ip so i can get the mac address. Every time i use the server variable in php i always get my public ip and not the local one. $_SERVER['REMOTE_ADDR'] returns my public ip, $_SERVER['X_FORWARDED_FOR'] not even exists and $_SERVER['SERVER_ADDR'] returns the ip of the VPS. I only connect to Api with hardcode ip but i cannot hardcode users ip. Any solution to this?

The web server also needs to be a member of the VPN, and (more importantly) be accessed through that VPN IP.

I understand you might want to have your web server ALSO be accessed via a public IP... But in that case, you'd want to dedicate a subdomain to which the A/AAAA record will be the VPN IP.

In webserver runs a VPN Client. Server is already connected to VPN. I can ping server from mikrotik and reverse with VPN ips.

But what about when you open up the PHP page from an end user device? In the browser, are you opening http://192.168.100.200/ (where that example IP is the VPN IP of the web server), or are you opening http://mydomain.com/ (where that example domain is a publically available domain from which anyone can access the web server)? If the latter, then mydomain.com would not work, unless it resolves to 192.168.100.200. If you need to keep it accessible from the outside, add a subdomain, f.e. panel.mydomain.com, make that subdomain resolve to 192.168.100.200, and from end user devices, open the server through that subdomain.

It is an external hotspot page. User hits the router hotspot, router hotspot redirects user to external hotspot page and external hotspot redirects user back to router hotspot. The problem is that external hotspot is already to a subdomain and i need it to be accessible from outside for other apis connections but also from mikrotik api for resolving mac address at the same time. I already added a subdomain name to hotspot with the ip that router gets from vpn server and post requests are fine. Api connection is fine. But no way to find users local ip. I even tried to read a file which prints the ip but with no hope.

A redirect is OK, but the PHP page the user is redirected to needs to use a subdomain that resolves to the VPN IP of the web server (if not the VPN IP itself, without a DNS name).

The external web server may be configured to be accessible from multiple IPs and (sub)domains. Certain pages (e.g. API related ones) can be configured to only open from certain subdomains, and certain other pages (e.g. those that need to also be accessible by 3rd parties, but otherwise don't interact with the router) may be configured to be accessible from all subdomains.

It's just that hotspot users need to use the VPN IP, or a subdomain that resolves to that IP.

I already added a domain name to hotspot with the ip that router gets from vpn server

Add a domain name to hotspot with the IP that the external web server gets from the VPN server, and redirect to that from hotspot.

Hi @boenrobot again. I have done what you told me to do. Now all users who are connected to mikrotik when they visit the external hotspot page they are doing it with the VPN ip that the server gets from the VPN network. The external hotspot page resolves via the web server vpn ip and not the public one. Although when i use $_SERVER['REMOTE_ADDR'] i still see user public ip and not the local one.
The path is: user->mikrotik->vpn->server . There i want to get with php the local ip from the user to get his mac address throught the api but i always get his public ip and not the local. Excuse me if i don't understand something simple.

To be perfectly honest, I've never tested any of that code through a VPN, so I wasn't sure if $_SERVER['REMOTE_ADDR'] would do the trick with it too... I just assumed it would, as I see no reason it shouldn't.

Upon some googling right now, I guess when using a VPN, you could use $_SERVER['HTTP_CLIENT_IP'] instead of $_SERVER['REMOTE_ADDR']. The latter supposedly always gives the "true" IP, while the former gives the IP the server was accessed, even if it's a VPN. I'd guess/hope there's also an equivalent for the client port (maybe $_SERVER['HTTP_CLIENT_PORT']?), as you do need that one too...

What does var_dump($_SERVER); show when hotspot client access it right now (via the web server's VPN IP)? Does the VPN IP of the router appear anywhere in the array?

"REMOTE_ADDR" => "..."public user ip
"REMOTE_PORT" => "51619"
"REQUEST_METHOD" => "GET"
"REQUEST_SCHEME" => "https"
"SERVER_ADDR" => "...."Server public ip
"SERVER_PORT" => "443"

Only these variables with ips or ports

The only possible issue I'm seeing is that the traffic isn't going through the VPN for some reason, but is routed over the real IP. This could happen if the client's DNS server is not your router for example (which is why I suggested you add a new subdomain at the web server's DNS server; where your public domain's IP is defined), or if you're reusing a pre-existing subdomain, and the DNS cache points to the public IP.

Are you sure the VPN IP is used? If from a hotspot client you call ping panel.mydomain.com, do you get the VPN IP of the web server as the resolved one?

I get the VPN ip, i use another mikrotik for vpn server not the same mikrotik for hotspot

What type of VPN are you using? OpenVPN?

At this point, the only way I could potentially help is by actually setting up a VPN in a GNS3 lab, and perhaps afterwards comparing my experience/settings with those in your network.