Android, ethernet and DHCP

Investigating an issue we had using DHCP with ethernet on Android, I found plenty of posts, threads or website showing their way to setup it with an ethernet interface. Knowing that Android natively supports it, at least since JellyBean, I decided to explain how it is implemented and the official way to configure it for ethernet.

Overview

Android uses dhcpcd, a dhcp client implemented as daemon (available in repo platform/external/dhcpcd) which is designed to monitor one or multiple interfaces, get addresses through dhcp, renew leases, etc. When the link status (not the interface one) changes from down to up, netd notifies the framework which launches, through the ConnectivityService and the EthernetDataTracker a DHCPCD instance on this interface. The service then waits for request completion (or failure) and propagate, if necessary, the “online” status to the whole framework.

How it works ?

Connectivity monitoring

Everything begins in the ConnectivityController constructor. A list of all desired connections is gathered from a ressource called com.android.internal.R.array.networkAttributes provided by a platform XML resource file (frameworks/base/core /res/res/values/config.xml). Usually this file is overridden by the device configuration overlay from the vendor. This file defines the kind of connection available on this platform, their priority, etc.

Always in this constructor, when an ethernet configuration is found in the array, it gets an instance of the EthernetDataTracker. The priority filed in the configuration helps the controller to choose which connection to use first. When an ethernet one is present, it often has the highest priority.

Those trackers, including the ethernet one, are used by the controller to monitor the state of the connections to external networks (i.e. the internet). It gets link status from them and depending on witch one is active (link up, ip configuration done) it reports internet connectivity status to the applications.

Ethernet interface management

The ethernet interface management is done through the EthernetDataTracker. The class implements the NetworkStateTracker interface used by the ConnectivityController to do its monitoring job. This singleton class is designed to handle only one interface at a time.

The instance is got by the ConnectivityController which triggers the startMonitoring() method. On this event the tracker connects to netd, the Android network daemon, through binders to get interfaces and, later, monitor their state.

/platform/frameworks/base/+/jb-dev/core/java/android/net/EthernetDataTracker.java
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
public void startMonitoring(Context context, Handler target) {
        mContext = context;
        mCsHandler = target;
        // register for notifications from NetworkManagement Service
        IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE);
        mNMService = INetworkManagementService.Stub.asInterface(b);
        mInterfaceObserver = new InterfaceObserver(this);
        // enable and try to connect to an ethernet interface that
        // already exists
        sIfaceMatch = context.getResources().getString(
            com.android.internal.R.string.config_ethernet_iface_regex);
        try {
            final String[] ifaces = mNMService.listInterfaces();
            for (String iface : ifaces) {
                if (iface.matches(sIfaceMatch)) {
                    mIface = iface;
                    mNMService.setInterfaceUp(iface);
                    InterfaceConfiguration config = mNMService.getInterfaceConfig(iface);
                    mLinkUp = config.isActive();
                    if (config != null && mHwAddr == null) {
                        mHwAddr = config.getHardwareAddress();
                        if (mHwAddr != null) {
                            mNetworkInfo.setExtraInfo(mHwAddr);
                        }
                    }
                    reconnect();
                    break;
                }
            }
        } catch (RemoteException e) {
            Log.e(TAG, "Could not get list of interfaces " + e);
        }
        try {
            mNMService.registerObserver(mInterfaceObserver);
        } catch (RemoteException e) {
            Log.e(TAG, "Could not register InterfaceObserver " + e);
        }
    }

The whole available interface list is gathered from netd and browsed to find one which matches ethernet interface pattern. This pattern is provided by a platform resource config_ethernet_iface_regex. When an interface is found, the tracker asks netd to set it up and breaks the search loop. Before breaking, it issues a reconnect() call which will eventually start the DHCP auto configuration.

DHCP

On reconnect() event, the runDhcp() call spawns a thread which will start the dhcp request using NetworkUtils.runDhcp(…). This method uses a native JNI implementation to launch a DHCPCD process and wait for its result (or failure) before return. The implementation is located in frameworks/base/core/jni/android_net_NetUtils.cpp which is a wrapper designed to use the netutils library (system/core/libnetutils/dhcp_utils.c). Everything is happening in dhcp_do_request:

/platform/system/core/+/jb-dev/libnetutils/dhcp_utils.c
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
/*
 * Start the dhcp client daemon, and wait for it to finish
 * configuring the interface.
 *
 * The device init.rc file needs a corresponding entry for this work.
 *
 * Example:
 * service dhcpcd_ /system/bin/dhcpcd -ABKL
 */
int dhcp_do_request(const char *interface,
                    char *ipaddr,
                    char *gateway,
                    uint32_t *prefixLength,
                    char *dns1,
                    char *dns2,
                    char *server,
                    uint32_t *lease,
                    char *vendorInfo)
{
    char result_prop_name[PROPERTY_KEY_MAX];
    char daemon_prop_name[PROPERTY_KEY_MAX];
    char prop_value[PROPERTY_VALUE_MAX] = {'\0'};
    char daemon_cmd[PROPERTY_VALUE_MAX * 2];
    const char *ctrl_prop = "ctl.start";
    const char *desired_status = "running";
    /* Interface name after converting p2p0-p2p0-X to p2p to reuse system properties */
    char p2p_interface[MAX_INTERFACE_LENGTH];
    get_p2p_interface_replacement(interface, p2p_interface);
    snprintf(result_prop_name, sizeof(result_prop_name), "%s.%s.result",
            DHCP_PROP_NAME_PREFIX,
            p2p_interface);
    snprintf(daemon_prop_name, sizeof(daemon_prop_name), "%s_%s",
            DAEMON_PROP_NAME,
            p2p_interface);
    /* Erase any previous setting of the dhcp result property */
    property_set(result_prop_name, "");
    /* Start the daemon and wait until it's ready */
    if (property_get(HOSTNAME_PROP_NAME, prop_value, NULL) && (prop_value[0] != '\0'))
        snprintf(daemon_cmd, sizeof(daemon_cmd), "%s_%s:-h %s %s", DAEMON_NAME, p2p_interface,
                 prop_value, interface);
    else
        snprintf(daemon_cmd, sizeof(daemon_cmd), "%s_%s:%s", DAEMON_NAME, p2p_interface, interface);
    memset(prop_value, '\0', PROPERTY_VALUE_MAX);
    property_set(ctrl_prop, daemon_cmd);
    if (wait_for_property(daemon_prop_name, desired_status, 10) < 0) {
        snprintf(errmsg, sizeof(errmsg), "%s", "Timed out waiting for dhcpcd to start");
        return -1;
    }
    /* Wait for the daemon to return a result */
    if (wait_for_property(result_prop_name, NULL, 30) < 0) {
        snprintf(errmsg, sizeof(errmsg), "%s", "Timed out waiting for DHCP to finish");
        return -1;
    }
    if (!property_get(result_prop_name, prop_value, NULL)) {
        /* shouldn't ever happen, given the success of wait_for_property() */
        snprintf(errmsg, sizeof(errmsg), "%s", "DHCP result property was not set");
        return -1;
    }
    if (strcmp(prop_value, "ok") == 0) {
        char dns_prop_name[PROPERTY_KEY_MAX];
        if (fill_ip_info(interface, ipaddr, gateway, prefixLength,
                dns1, dns2, server, lease, vendorInfo) == -1) {
            return -1;
        }
        /* copy dns data to system properties - TODO - remove this after we have async
         * notification of renewal's */
        snprintf(dns_prop_name, sizeof(dns_prop_name), "net.%s.dns1", interface);
        property_set(dns_prop_name, *dns1 ? ipaddr_to_string(*dns1) : "");
        snprintf(dns_prop_name, sizeof(dns_prop_name), "net.%s.dns2", interface);
        property_set(dns_prop_name, *dns2 ? ipaddr_to_string(*dns2) : "");
        return 0;
    } else {
        snprintf(errmsg, sizeof(errmsg), "DHCP result was %s", prop_value);
        return -1;
    }
}

The whole function is relies on a DHCPCD service usually declared like this in the init.rc platform file:

service dhcpcd_eth0 /system/bin/dhcpcd -ABDKL
    class main
    disabled
    oneshot

First the function starts this service setting the property ctl.start to dhcpcd_eth0:-f /path/to/configuration/file.conf -d eth0. This starts the service using the Android usual way and adds some parameters to initial init.rc declaration. The implementation now waits for the property init.svc.dhcpcd to check if the service is running and then waits for dhcp.eth0.result to check if the service ended and get a result, or not. The success of the failure is returned to callers, all along the call stack which ends in the famous EthernetDataTracker.

If the whole DHCP request fails for any reason, the process will be restarted on next interface up event.

Conclusion

Android is able to manage an ethernet interface with DHCP. The implementation is a bit simple but its enough for almost all cases. The only drawback we found is the DHCP timeout which is a bit short. Indeed if your network has a switch not configured in fast port mode, the request may timeout before the switch let the traffic go through himself. Thus you won’t ever be able to get an address, except if you re-trigger the service using ctl.start. But this is another story, for an other article 😉