cnetwork-programmingnetlink

Changing mac&ip address of eth0 lancard using libnl3 netlink library with C


I'm writing a code to change IP & mac address of eth0 lan card using netlink library. But my work cannot go forward...

My code logic is

  1. Make a netlink socket
  2. Bind to kernel
  3. make a cache and get the network devices information.
  4. get the eth0 object to orig_link
  5. get the eth0 object to new_link and set the ip address
  6. change the link to orig_link -> new_link
#include <netlink/netlink.h>
#include <netlink/socket.h>
#include <netlink/route/link.h>
#include <netlink/route/addr.h>
#include <netlink/route/route.h>
#include <netlink/route/addr.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>

int main() 
{
    int err;
    struct nl_sock *sock = nl_socket_alloc();

    nl_connect(sock, NETLINK_ROUTE);

    // === Cache allocation ===
    struct nl_cache *cache;
    rtnl_link_alloc_cache(sock, AF_UNSPEC, &cache); //cache holds all the NI config.
    
    // === Link object allocation for eth0 ===
    struct rtnl_link *orig_link = rtnl_link_get_by_name(cache, "eth0"); 
    if (!orig_link) {
        fprintf(stderr, "Failed to allocate link for eth0.\n");
        nl_socket_free(sock);
        return 1;
    }

    struct rtnl_link *new_link = rtnl_link_get_by_name(cache, "eth0");
    const char *new_ip = "myip"; // this field should be changed to my IP address
    struct nl_addr *addr;

    if(!nl_addr_valid(new_ip, AF_INET)) //validation check
    {
        printf("Validation check : Invalid ip address!\n");
        return 1;
    }
    if (nl_addr_parse(new_ip, AF_INET, &addr) < 0) { 
        fprintf(stderr, "Parsing check : Invalid ip address format.\n");
        return 1;
    }

    // === Apply addr into link object ===
    rtnl_link_set_addr(new_link, addr); 

    err = rtnl_link_change(sock, orig_link, new_link, NLM_F_CREATE);
    if (err < 0) 
    {
        fprintf(stderr, "Failed to apply ip address change... Err is %s\n", nl_geterror(err));
    }

    nl_addr_put(addr); 
    nl_cache_free(cache);
    rtnl_link_put(orig_link);
    rtnl_link_put(new_link);
    nl_socket_free(sock);
    return 0;
}

for the rtnl_link_change function flags, each flags give me below err message.

NLM_F_CREATE : Invalid input data or parameter
NLM_F_REQUEST : Invalid input data or parameter
NLM_F_REPLACE : Invalid input data or parameter
NLM_F_EXCL : Object exists

I've replaced the function rtnl_link_change to rtnl_link_add only using orig_link object but it didn't work, too.

How to fix the error?


Solution

  • You are mixing up two separate kinds of "addresses".

    rtnl_link_set_addr() is not for AF_INET addresses. It sets the link-layer address – e.g. for Ethernet links that would be the 6-byte Ethernet MAC address.

    Within Netlink, link-layer (MAC) addresses are a property of the link (because a link always has exactly one), but network-layer (IP) addresses are not – they are standalone objects which are associated with some interface by its ifIndex. See struct rtnl_addr in libnl docs.

    This is because a link is not limited to a single IP address, i.e. there is no "the" address that you would "change"; it's common for a single interface to have multiple AF_INET addresses (and of course multiple AF_INET6 addresses). So instead you have to remove all old addresses (delete the old rtnl_addr objects), then add a new address.

    (This would correspond 1:1 to ip addr del... && ip addr add... in the Netlink-based iproute2 CLI, whereas your current code corresponds to ip link set eth0 addr $IP.)

    Remember that it is not enough to specify just the address – the OS needs to know the "netmask" associated with it (i.e. the subnet size). In Netlink this is a property of the rtnl_address, set through rtnl_addr_set_prefixlen() (e.g. 255.255.255.0 would correspond to prefix length 24). Do not hardcode the prefix length.

    Likewise, "default gateway" is specified through route objects, specifically ones where the destination is 0.0.0.0/0 or ::/0. To "change" the default gateway you need to delete all default routes for that protocol (INET or INET6), then add a new one.


    IPv6-specific note: While in IPv4 you can safely "flush" all old addresses, in IPv6 you must make sure to avoid accidentally deleting the "link scope" 'fe80::xxx' address, as at least one is required for IPv6 to remain operational. In the CLI this would be done using ip -6 addr flush dev eth0 scope global (as 'global' is the only other scope in practical use), but in Netlink API you would have to loop over each old address and check rtnl_addr_get_scope() != RT_SCOPE_LINK before deleting it.