Search code examples
c#monosambalibsmbclient

Mono DllImport libsmbclient not authenticating


I need to be able to access a Samba/Cifs share from Mono using specific credentials.

The best option I've found so far is to use libsmbclient. Unfortunately, I can't get it to authenticate. To rule out firewall/security/etc, I've tried using the smbclient executable which can connect without issue.

The low-level DLLImport stuff and some hard-coded creds for testing...

public static void Initialise() {
    log.Trace("Initialising libsmbclient wrapper");
    try {
        smbc_init(callbackAuth, 1);
    } catch (Exception e) {
        log.Trace(String.Format("{0}: {1}", e.GetType().Name, e.ToString()));
        throw;
    }
}

public static void callbackAuth(
    [MarshalAs(UnmanagedType.LPStr)]String server,
    [MarshalAs(UnmanagedType.LPStr)]String share,
    [MarshalAs(UnmanagedType.LPStr)]String workgroup, int workgroupMaxLen,
    [MarshalAs(UnmanagedType.LPStr)]String username, int usernameMaxLen,
    [MarshalAs(UnmanagedType.LPStr)]String password, int passwordMaxLen) {
    server = "targetserver";
    share = "Public";
    username = "Management.Service";
    password = @"{SomeComplexPassword}";
    workgroup = "targetserver";
    usernameMaxLen = username.Length;
    passwordMaxLen = password.Length;
    workgroupMaxLen = workgroup.Length;
}

[DllImport("libsmbclient.so", SetLastError = true)]
extern internal static int smbc_init(smbCGetAuthDataFn callBackAuth, int debug);

[DllImport("libsmbclient.so", SetLastError = true)]
extern internal static int smbc_opendir([MarshalAs(UnmanagedType.LPStr)]String durl);

I'm attempting to use it like this...

public bool DirectoryExists(string path) {
    log.Trace("Checking directory exists {0}", path);
    int handle;
    string fullpath = @"smb:" + parseUNCPath(path);
    handle = SambaWrapper.smbc_opendir(fullpath);
    if (handle < 0) {
    var error = Stdlib.GetLastError().ToString();
        if (error == "ENOENT"
           |error == "EINVAL")
            return false;
        else
            throw new Exception(error);
    } else {
    WrapperSambaClient.smbc_close(fd);
    return true;
}
}

the Initialise() call succeeds but when DirectoryExists calls SambaWrapper.smbc_opendir(fullpath) I get a negative handle and throw the following exception...

Slurpy.Exceptions.FetchException: Exception: Failed to fetch: EACCES > (file:////targetserver/Public/ValidSubfolder) ---> System.Exception: EACCES

What am I doing wrong? Is there any way I can debug this?

Edit: It seems the issues is that the values from the auth callback have no effect (but the callback is definitely being called as a log statement in there is processed). I'm wondering if it's something to do with the immutability of strings and a new string instance being created with the new values rather than overwriting the old?

Edit: Full debug output from libsmbclient attempting to connect removed. You can see it in the edit history if required.

As requested, the definition of the method from the headers...

/**@ingroup misc
 * Initialize the samba client library.
 *
 * Must be called before using any of the smbclient API function
 *  
 * @param fn        The function that will be called to obtaion 
 *                  authentication credentials.
 *
 * @param debug     Allows caller to set the debug level. Can be
 *                  changed in smb.conf file. Allows caller to set
 *                  debugging if no smb.conf.
 *   
 * @return          0 on success, < 0 on error with errno set:
 *                  - ENOMEM Out of memory
 *                  - ENOENT The smb.conf file would not load
 *
 */

int smbc_init(smbc_get_auth_data_fn fn, int debug);

/**@ingroup callback
 * Authentication callback function type (traditional method)
 * 
 * Type for the the authentication function called by the library to
 * obtain authentication credentals
 *
 * For kerberos support the function should just be called without
 * prompting the user for credentials. Which means a simple 'return'
 * should work. Take a look at examples/libsmbclient/get_auth_data_fn.h
 * and examples/libsmbclient/testbrowse.c.
 *
 * @param srv       Server being authenticated to
 *
 * @param shr       Share being authenticated to
 *
 * @param wg        Pointer to buffer containing a "hint" for the
 *                  workgroup to be authenticated.  Should be filled in
 *                  with the correct workgroup if the hint is wrong.
 * 
 * @param wglen     The size of the workgroup buffer in bytes
 *
 * @param un        Pointer to buffer containing a "hint" for the
 *                  user name to be use for authentication. Should be
 *                  filled in with the correct workgroup if the hint is
 *                  wrong.
 * 
 * @param unlen     The size of the username buffer in bytes
 *
 * @param pw        Pointer to buffer containing to which password 
 *                  copied
 * 
 * @param pwlen     The size of the password buffer in bytes
 *           
 */
typedef void (*smbc_get_auth_data_fn)(const char *srv, 
                                      const char *shr,
                                      char *wg, int wglen, 
                                      char *un, int unlen,
                                      char *pw, int pwlen);

Solution

  • your callback gets invoked by libsmbclient, and it is passing buffer and its length from unmanaged code, expecting you to populate username and password. Since this allocated memory is controlled by calling side, you cannot use string or stringbuilder. I suggest to use IntPtr, and populate buffer on lowest possible level.

    (On the other hand, server and share are read-only strings, and not expected to change, so we can keep them string).

    public static void callbackAuth(
        [MarshalAs(UnmanagedType.LPStr)]String server,
        [MarshalAs(UnmanagedType.LPStr)]String share,
        IntPtr workgroup, int workgroupMaxLen,
        IntPtr username, int usernameMaxLen,
        IntPtr password, int passwordMaxLen) 
    {
        //server = "targetserver";
        //share = "Public"; 
        // should not be assigned - 
        // you must provide credentials for specified server
    
        SetString(username, "Management.Service", username.Length);
        SetString(password, @"{SomeComplexPassword}", password.Length);
        SetString(workgroup, "targetserver", workgroup.Length);
    }
    
    private void SetString(IntPtr dest, string str, int maxLen)
    {
        // include null string terminator
        byte[] buffer = Encoding.ASCII.GetBytes(str + "\0");
        if (buffer.Length >= maxLen) return; // buffer is not big enough
    
        Marshal.Copy(buffer, 0, dest, buffer.Length);
    }