Getting metadata (identity) about an SMB volume's server

I like to find a way to identify network volumes, and whether they're run by certain servers, e.g. specifically whether they're on a Synology NAS.

Reason is that Synology, while apparently supporting the Spotlight-over-SMB API, comes with a lot of bugs, requiring me to work around them when searching on those volumes with the macOS Spotlight API.

I could, of course, ask the user to "configure" each mounted volume in my software, but I'd rather do this automagically, if possible, as it's less prone to user mistakes.

So, my question is: Is there a way to learn a bit more about the server of a mounted network volume? E.g., if I could learn its IP address, I could try to connect to it via http protocol and then maybe get a useful response that identifies it as being from Synology.

Or, alternatively, can I tell which SMB volumes are served by a Mac, so that I can at least assume that those handle Spotlight calls correctly, while I assume anything else is buggy (so far, AFAIK, Synology is the only other SMB server that supports Spotlight search).

I've tried to find some data in the IORegistry, but that doesn't seem to store anything about network vols. The statfs function doesn't seem to give me anything for that either, nor do the various fcntl calls as far as I could tell.

I also checked with the DA apis, e.g.:

DASessionRef daSession = DASessionCreate (NULL);
CFURLRef furl = CFURLCreateWithFileSystemPath(NULL, CFSTR("/Volumes/TheNAS"), kCFURLPOSIXPathStyle, true);
DADiskRef daDisk = DADiskCreateFromVolumePath (NULL, daSession, furl);
if (daDisk) {
  CFDictionaryRef daInfo = DADiskCopyDescription (daDisk);
  NSLog(@"%@", daInfo);
}

However, this only prints basic information:

DAVolumeKind = smbfs;
DAVolumeMountable = 1;
DAVolumeName = TheNAS;
DAVolumeNetwork = 1;
DAVolumePath = "file:///Volumes/TheNAS/";

Where, then, does Finder's "Get Info" get the smb path from, for example?

Answered by DTS Engineer in 774521022

This is on the Mac, right? If so, NetFSCopyURLForRemountingVolume returns the URL used to mount the volume. You can get the host part of that and away you go.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Accepted Answer

This is on the Mac, right? If so, NetFSCopyURLForRemountingVolume returns the URL used to mount the volume. You can get the host part of that and away you go.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Alright, I figured out a way to identify the server by contacting it via https and then checking its certificate. That should be pretty reliable.

Below the code I use, though I ran into one complication: The mount URL I get for my QNAP NAS is: "QNAS._smb._tcp.local", but that is not a valid host name I can use in a NSURLRequest!

I need to transform that into the actual host name, which is "QNAS.local". Since the former is a Bonjour related name, and when I browse the Bonjour registry with BonJeff, I can find the mapping, this seems to be an overly complicated method. I wonder what the proper way is to get the basic host name from such a service name. I've googled for a while but could not find anything about it. For now, I simply remove all components from the host name that start with an underscore, but I'm not sure if that's a safe method.

#import <NetFS/NetFS.h>
#import <Security/Security.h>
#import "AppDelegate.h"

@interface AppDelegate () <NSURLSessionDelegate>
@end

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
	CFURLRef furl = CFURLCreateWithFileSystemPath(NULL, CFSTR("/Volumes/TheNAS"), kCFURLPOSIXPathStyle, true);
	NSURL *url = CFBridgingRelease(NetFSCopyURLForRemountingVolume (furl));
	NSArray *parts = [[url.host componentsSeparatedByString:@"."] filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSString* _Nullable part, id _Nullable bindings) {
		return ![part hasPrefix:@"_"];
	}]];
	NSString *addr = [parts componentsJoinedByString:@"."];
	NSLog(@"host: %@ -> %@", url.host, addr);

	NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
	[request setURL:[NSURL URLWithString:[NSString stringWithFormat:@"https://%@", addr]]];
	NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil];
	[[session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
		NSLog(@"Done.");
		[NSApp terminate:self];
	}] resume];
}

-(void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler
{
	SecTrustRef trustRef = [[challenge protectionSpace] serverTrust];
	SecCertificateRef certRef = SecTrustGetCertificateAtIndex(trustRef, 0);
	CFStringRef name = nil;
	SecCertificateCopyCommonName(certRef, &name);
	NSLog(@"name: %@", name);

	// reject the challenge because we have all we wanted
	completionHandler (NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}

@end

The mount URL I get for my QNAP NAS is: QNAS._smb._tcp.local

Oh, wow, that’s the name of an SRV record. I wasn’t expecting that.

I wonder what the proper way is to get the basic host name from such a service name.

I recommend that you use the DNS-SD API for this. To get started, look at the SRVResolver sample code.

IMPORTANT That sample predates DNSServiceSetDispatchQueue, which greatly simplifies this whole business. I recommend that you combine the SRV concepts it shows with some code that uses DNSServiceSetDispatchQueue, such as DNSSDObjects.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

@eskimo, With the code from SRVResolver I can indeed resolve QNAS._smb._tcp.local. Problem is: If the host name is a plain one, like syno.local, then I get neither an error nor a callback. And using a Timeout for a local-only NS resolve seems wrong to me.

How should I best handle this? Is there a way to tell when I need to use the SRV record resolver?

Is there a way to tell when I need to use the SRV record resolver?

Not as such. However, it’s easy to distinguish the two cases by looking for the ._smb._tcp. pattern. This can’t appear in an IP address or a standard DNS host name (_ is not a valid character for DNS host name).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

@eskimo,

There's a new problem I just ran into:

If the host is a Windows server, the domain name of the mounted volume resolve to a name that I cannot use - neither directly nor with the proposed SRVResolver.

For instance, I set up a Windows server with the netbios(?) name "win-serv", and a volume named "WinServer". When I Get Info on the mounted volume, it shows as:

smb://win-serv/WinServer

When I retrieve the domain name, e.g. with statfs, the name I get is just "win-serv", but that can't be used as an IP address.

I found that I can resolve this name with the shell cmd smbutil lookup win-serv, though.

So my questions are:

  1. Is there an API for doing what smbutil lookup does?
  2. How do I tell that I need to use this method? I guess I could just always fall back to it when I cannot reach the server via its given name, but is there a clearer indicator that this is a WINS(?) and not a usual DNS name?

I have a related question on StackOverflow but that hasn't gotten me anywhere, either. This entire WINS/NetBIOS thing is a mystery on macOS, it seems.

1. Is there an API for doing what smbutil lookup does?

Not that I’m aware of.

2. How do I tell that I need to use this method?

I can’t think of any good options for that.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

BTW, I found out that taking the common name from the certificate is not a reliable identification method to identify Synology NAS systems, because their cert can be self-signed with a custom name.

Instead, Synology NAS can be reliably located with the SSDP protocol. I've published code for this here: https://github.com/tempelmann/SSDP-Browser

To get started, look at the SRVResolver sample code.

In case others run into this, let me add this warning about the SRVResolver code:

In the function QueryRecordCallback, the code first makes some assertions before checking the errorCode. That's likely to cause problems because if you get an error (such as if a user doesn't give your app permission to use the network calls), the asserts may get triggered. To fix this, move the assert calls inside the code that checks for error == 0.

Getting metadata (identity) about an SMB volume's server
 
 
Q