Static IP configured via app

We have a scenario where we need to ship flashed devices to customers. They will plug the device into ethernet initially which will provision the device. They would have the option to switch to a wifi connection which we are using resin-wifi-connect for. That piece works pretty well.

However we have another requirement that the customer be able to configure a static ip for their connection, whether it be wired or wireless. The customer will not be able to take the SD card and mount it to modify the connections as the instructions for configuring a static IP note. It needs be configurable via an app. Something user friendly that anyone could use and understand.

Our first thought was to modify/enhance resin-wifi-connect. We’ve taken numerous stabs at this without any luck. We’ve attempted numerous libs/tools via node, python, shell scripts that use dbus to add/modify connections and have come up empty. The main problem seems to be that when switching the ip, resin loses its connection to the device and is never able to recover it and the device goes offline. (thats what happens majority of the time, rarely it stays online with a new ip but nothing from the resin side works. for instance, we cant restart the application, environment variable changes have no effect, etc…)

So our question is whether this type of thing is even possible or supported with resin? If its possible/supported, can anyone steer us in the right direction to help us get this working? Has anyone had similar requirements and if so how did you handle it?

Can you help me understand the setup when you configure the static IP? Are you modifying the ethernet connection, or are you modifying the configuration of the wifi device? And if it’s the wifi device, are you also connected via ethernet while modifying the connection?

Finally, do you have a code example of how you have tried to do this?

Sure

So the answer is basically either. When they are plugged into the ethernet initially and it launches the modified resin-wifi-connect, one option they would have is to stick with ethernet and change the IP to what they would like if they would like to. The other option would be to configure the wifi ssid and password and change the IP to what they would like if they would like to.

So one of the scenarios would be modifying the ethernet connection. The other scenario would be creating the wifi connection if they choose to use wifi. If they are creating the wifi configuration they would still be connected via ethernet.

Which is a good point. Reflecting back on the past week and what seems like thousands of trials and errors. Most of the struggles have been with modifying the ethernet. I’ll need to go back and confirm but I don’t think configuring the wifi has had the disconnection issues presumably because the ethernet connection keeps the device connected as the wifi connection is setup.

I think I should be able to get some code. Ive got numerous examples of attempts at this. Give me a little bit to make sure its cleaned of any client info and I’ll post

this is one of the attempts. its node calling python script to do the work. python uses package python-network manager

new Promise (resolve, reject) ->
	pythonCallback = (err, results) ->
		console.log('In python callback')
		if err
			console.log(err)
			reject(err)
		console.log('results: %j', results)
		resolve()

	console.log('Running Python')
	console.log('scriptPath: ' + __dirname + '/../python')

	options = {
		mode: 'text',
		scriptPath: __dirname + '/../python',
		args: [ip, cidr, gateway, dns1, dns2]
	};

	PythonShell.run('addconnection.py', options, pythonCallback)

addconnection.py

"""
Add a connection to NetworkManager. You do this by sending a dict to
AddConnection. The dict below was generated with n-m dump on an existing
connection and then anonymised
"""

import NetworkManager
import uuid
import argparse
import time

parser = argparse.ArgumentParser(description='Add connection via network manager')
parser.add_argument('ip', type=str, help='ip address')
parser.add_argument('cidr', type=int, help='cidr')
parser.add_argument('gateway', type=str, help='gateway address')
parser.add_argument('dns1', type=str, help='dns address')
parser.add_argument('dns2', type=str, help='dns address')

args = parser.parse_args()

settings = {
	"802-3-ethernet": {
		"auto-negotiate": True
	},
	"connection": {
		"id": "eth0",
		"type": "802-3-ethernet",
		'uuid': str(uuid.uuid4())
	},
	"ipv4": {
		"method": "manual",
		"addresses": [
			[args.ip, args.cidr, args.gateway]
		],
		"dns": [args.dns1, args.dns2]
	}
}

def waitForConnection():
        while NetworkManager.NetworkManager.State < NetworkManager.NM_STATE_CONNECTED_LOCAL:
            time.sleep(0.5)

def waitForDisconnection():
	while NetworkManager.NetworkManager.State >= NetworkManager.NM_STATE_CONNECTED_LOCAL:
		time.sleep(0.5)

active = NetworkManager.NetworkManager.ActiveConnections[0]
ap = active.SpecificObject
dev = active.Devices[0]
conn = active.Connection
conn.Delete()
waitForDisconnection()


conn = NetworkManager.Settings.AddConnection(settings)
NetworkManager.NetworkManager.ActivateConnection(conn, dev, ap)
waitForConnection()

print 'true'

this is one using shelljs to basically utilize nmcli to do the work. this one is ugly

new Promise (resolve, reject) ->
		console.log('Saving Wired Static Connection')
		disconnectCmd = 'nmcli dev disconnect eth0'
		removeCmd = 'nmcli connection delete id eth0'
		addCmd = 'nmcli con add type ethernet con-name eth0 ifname eth0 ip4 ' + ip + '/' + cidr + ' gw4 ' + gateway
		modCmd = 'nmcli con mod eth0 ipv4.dns "' + dns1 + ' ' + dns2 + '"'
		upCmd = 'nmcli connection up id eth0'
		restartCmd = 'service network-manager restart'

		console.log(addCmd)
		exec(addCmd, (addCode, addStdout, addStderr) ->
			if addCode != null
				console.log('Code: ' + addCode)
				console.log('Program Error: ' + addStderr)
				console.log('Program Output: ' + addStdout)
				reject(addStderr)
			else
				console.log(addStdout)
				console.log(modCmd)
				exec(modCmd, (modCode, modStdout, modStderr) ->
					if modCode != null
						console.log('Code: ' + modCode)
						console.log('Program Error: ' + modStderr)
						console.log('Program Output: ' + modStdout)
						reject(modStderr)
					else
						console.log(modStdout)
						console.log('Saved Wired Static Connection')
						console.log('Disconnecting interface')
						exec(disconnectCmd, (dcCode, dcStdout, dcStderr) ->
							if dcCode != null
								console.log('Code: ' + dcCode)
								console.log('Program Error: ' + dcStderr)
								console.log('Program Output: ' + dcStdout)
								reject(dcStderr)
							else
								console.log(dcStdout)
								console.log('Interface Disconnected')
								console.log('Removing Old Connection')
								exec(removeCmd, (rmCode, rmStdout, rmStderr) ->
									if rmCode != null
										console.log('Code: ' + rmCode)
										console.log('Program Error: ' + rmStderr)
										console.log('Program Output: ' + rmStdout)
										reject(rmStderr)
									else
										console.log(rmStdout)
										console.log('Old Connection Removed')
										console.log('Bringing New Connection Up')
										exec(upCmd, (upCode, upStdout, upStderr) ->
											if upCode != null
												console.log('Code: ' + upCode)
												console.log('Program Error: ' + upStderr)
												console.log('Program Output: ' + upStdout)
												reject(upStderr)
											else
												console.log(upStdout)
												console.log('Connection Brought Up')
												console.log('Restarting Network Management Service')
												exec(restartCmd, (rsCode, rsStdout, rsStderr) ->
													if rsCode != null
														console.log('Code: ' + rsCode)
														console.log('Program Error: ' + rsStderr)
														console.log('Program Output: ' + rsStdout)
														reject(rsStderr)
													else
														console.log(rsStdout)
														console.log('Restarted Network Management Service')

														setTimeout(resolve, 5000)
												)
										)
								)
						)
				)
		)

This one is kinda mimicking how resin-wifi-connect sets up the wifi connection

connection = {
	'802-3-ethernet': {
		'auto-negotiate': true
	},
	'connection': {
		'id': 'eth0',
		'type': '802-3-ethernet',
		'uuid': uuidV4()
	},
	'ipv4': {
		'method': 'manual',
		'addresses': [[exports.dot2num(ip), cidr, exports.dot2num(gateway)]],
		'dns': [exports.dot2num(dns1), exports.dot2num(dns2)]
	}
}

bus.getInterfaceAsync(SERVICE, '/org/freedesktop/NetworkManager/Settings', 'org.freedesktop.NetworkManager.Settings')
.then (settings) ->
	settings.AddConnectionAsync(connection)
.then ->
	execAsync('sync')

I should also note, these are snippets of where things eventually wound up. They each went through many iterations and attempts. I dont think I could possibly post everything we tried over the span of the past week. But hopefully this gives some idea of what we were trying

Rather than deleting and recreating the connection, have you tried updating it? For example, there is a Python script that illustrates using the Update() method on the NetworkManager dbus connection here: https://cgit.freedesktop.org/NetworkManager/NetworkManager/tree/examples/python/dbus/update-ip4-method.py

I can also try to reproduce this and see if I get similar behavior but updating the connection might be the faster way to accomplish this.

(You’ll need to know the UUID of the connection you want to update, but this can also be retrieved from NetworkManager, e.g. with https://cgit.freedesktop.org/NetworkManager/NetworkManager/tree/examples/python/dbus/list-connections.py)

Appreciate the help @mccollam

So we hadn’t tried updating. The primary reason for that is that Wired connection 1 is in the resin-wifi-connect whitelist, so without a new connection that app doesn’t see “new” credentials and thus continually fires up the access point to set up a connection.

However, we don’t necessarily have to use resin-wifi-connect so that shouldn’t stop me from attempting an update instead. Maybe we are trying to jam a square peg in a round hole.

I will give the update method a try. Hopefully tomorrow morning. Im then traveling until mid next week so I may be a bit in and out when it comes to responding after tomorrow morning.

I have returned from travel and spent the evening attempting the route of updating the existing connection. I put together the following script. It does appear to successfully update the connection but it doesn’t seem to actually take effect. You will be able to see in the logging that the details have updated. But the IP of the device never actually changes.

import argparse
import socket
import struct

import dbus

# This example asks settings service for all configured connections.
# It also asks for secrets, demonstrating the mechanism the secrets can
# be handled with.

bus = dbus.SystemBus()


def ip_to_int(ip_string):
    return struct.unpack("=I", socket.inet_aton(ip_string))[0]


def int_to_ip(ip_int):
    return socket.inet_ntoa(struct.pack("=I", ip_int))


def merge_secrets(proxy, config, setting_name):
    try:
        # returns a dict of dicts mapping name::setting, where setting is a dict
        # mapping key::value.  Each member of the 'setting' dict is a secret
        secrets = proxy.GetSecrets(setting_name)

        # Copy the secrets into our connection config
        for setting in secrets:
            for key in secrets[setting]:
                config[setting_name][key] = secrets[setting][key]
    except Exception, e:
        pass


def print_connection_settings(search_uuid):
    # Ask the settings service for the list of connections it provides
    service_name = "org.freedesktop.NetworkManager"
    proxy = bus.get_object(service_name, "/org/freedesktop/NetworkManager/Settings")
    settings = dbus.Interface(proxy, "org.freedesktop.NetworkManager.Settings")
    connection_paths = settings.ListConnections()

    # List each connection's name, UUID, and type
    for path in connection_paths:
        con_proxy = bus.get_object(service_name, path)
        settings_connection = dbus.Interface(con_proxy, "org.freedesktop.NetworkManager.Settings.Connection")
        config = settings_connection.GetSettings()

        # Now get secrets too; we grab the secrets for each type of connection
        # (since there isn't a "get all secrets" call because most of the time
        # you only need 'wifi' secrets or '802.1x' secrets, not everything) and
        # merge that into the configuration data
        merge_secrets(settings_connection, config, '802-11-wireless')
        merge_secrets(settings_connection, config, '802-11-wireless-security')
        merge_secrets(settings_connection, config, '802-1x')
        merge_secrets(settings_connection, config, 'gsm')
        merge_secrets(settings_connection, config, 'cdma')
        merge_secrets(settings_connection, config, 'ppp')

        # Get the details of the 'connection' setting
        s_con = config['connection']
        connection_uuid = s_con['uuid']

        if connection_uuid == search_uuid:
            print config


def get_connection_uuid(search_id):
    # Ask the settings service for the list of connections it provides
    service_name = "org.freedesktop.NetworkManager"
    proxy = bus.get_object(service_name, "/org/freedesktop/NetworkManager/Settings")
    settings = dbus.Interface(proxy, "org.freedesktop.NetworkManager.Settings")
    connection_paths = settings.ListConnections()

    # List each connection's name, UUID, and type
    for path in connection_paths:
        con_proxy = bus.get_object(service_name, path)
        settings_connection = dbus.Interface(con_proxy, "org.freedesktop.NetworkManager.Settings.Connection")
        config = settings_connection.GetSettings()

        # Now get secrets too; we grab the secrets for each type of connection
        # (since there isn't a "get all secrets" call because most of the time
        # you only need 'wifi' secrets or '802.1x' secrets, not everything) and
        # merge that into the configuration data
        merge_secrets(settings_connection, config, '802-11-wireless')
        merge_secrets(settings_connection, config, '802-11-wireless-security')
        merge_secrets(settings_connection, config, '802-1x')
        merge_secrets(settings_connection, config, 'gsm')
        merge_secrets(settings_connection, config, 'cdma')
        merge_secrets(settings_connection, config, 'ppp')

        # Get the details of the 'connection' setting
        s_con = config['connection']
        connection_id = s_con['id']

        if connection_id == search_id:
            return s_con['uuid']

    return False


def update_connection(uuid, method, ip, prefix, gateway, dns1, dns2):
    bus = dbus.SystemBus()
    proxy = bus.get_object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/Settings")
    settings = dbus.Interface(proxy, "org.freedesktop.NetworkManager.Settings")

    for c_path in settings.ListConnections():
        c_proxy = bus.get_object("org.freedesktop.NetworkManager", c_path)
        c_obj = dbus.Interface(c_proxy, "org.freedesktop.NetworkManager.Settings.Connection")
        c_settings = c_obj.GetSettings()

        # Look for the requested connection UUID
        if c_settings['connection']['uuid'] != uuid:
            continue

        print c_settings

        # add IPv4 setting if it doesn't yet exist
        if 'ipv4' not in c_settings:
            c_settings['ipv4'] = {}

        # clear existing address info
        if c_settings['ipv4'].has_key('addresses'):
            del c_settings['ipv4']['addresses']
        if c_settings['ipv4'].has_key('address-data'):
            del c_settings['ipv4']['address-data']
        if c_settings['ipv4'].has_key('gateway'):
            del c_settings['ipv4']['gateway']

        # set the method and change properties
        c_settings['ipv4']['method'] = method
        if method == "manual":
            # Add the static IP address, prefix, and gateway
            addr1 = dbus.Array([ip_to_int(ip), dbus.UInt32(int(prefix)), ip_to_int(gateway)],
                               signature=dbus.Signature('u'))
            c_settings['ipv4'][dbus.String(unicode('addresses'))] = dbus.Array([addr1], signature=dbus.Signature('au'))
            c_settings['ipv4'][dbus.String(unicode('dns'))] = dbus.Array([ip_to_int(dns1), ip_to_int(dns2)],
                                                                         signature=dbus.Signature('u'))

            # Save all the updated settings back to NetworkManager
            c_obj.Update(c_settings)

        return True


parser = argparse.ArgumentParser(description='Add connection via network manager')
parser.add_argument('ip', type=str, help='ip address')
parser.add_argument('cidr', type=int, help='cidr')
parser.add_argument('gateway', type=str, help='gateway address')
parser.add_argument('dns1', type=str, help='dns address 1')
parser.add_argument('dns2', type=str, help='dns address 2')

args = parser.parse_args()

connection_uuid = get_connection_uuid('eth0')

if connection_uuid:
    result = update_connection(connection_uuid, 'manual', args.ip, args.cidr, args.gateway, args.dns1, args.dns2)
    print result
    print_connection_settings(connection_uuid)
else:
    print False

Example of calling this script

python updateconnection.py 192.168.2.92 24 192.168.2.1 8.8.8.8 8.8.4.4

Maybe ReloadConnections as seen in the following link is the answer. Will try that this morning.

https://developer.gnome.org/NetworkManager/0.9/spec.html#org.freedesktop.NetworkManager.Settings

Alright, I quickly tried ReloadConnections and I also tried manually restarting network manager using

service network-manager restart

Neither caused the IP to change despite the IP being setup when looking at the connection settings

I’ve tried the script you’ve pasted in on a resinOS 2.0.8 device I had at hand and seems to be replacing the settings and applies those just fine in my limited testing. Do you have any more context or more complete application example that you are trying so we can test closer to your setup?

I threw together the following repo quick

I stripped the script out our application and made a bare minimum install using it. When I run this on a new resinOS 2.0.8 I can see from the settings printouts that the ip was set, but the ip of the device does not change. It was set to change to 192.168.2.92

Hey @ljanssen I was checking this out, and this code still correctly updates the settings (and can see it in the host’s logs), for example:

The problem is that the active connection is not reloaded. This you can achieve brutally by rebooting (then the last setting applied would be the one that the device comes back up with) (probably easy but not recommended), or through DBUS, using NetworkManager.ActivateConnection:

The minimal code I have here, that works, adding to your code, to reactivate (eg. ethernet on eth0):

def reactivate(interface_name="eth0"):
    bus = dbus.SystemBus()
    proxy = bus.get_object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
    nm = dbus.Interface(proxy, "org.freedesktop.NetworkManager")
    devpath = nm.GetDeviceByIpIface(interface_name)
    nm.ActivateConnection('/', devpath, '/')

and thus adding a function call to your main code works for me:

if connection_uuid:
    result = update_connection(connection_uuid, 'manual', args.ip, args.cidr, args.gateway, args.dns1, args.dns2)
    print result
    print_connection_settings(connection_uuid)
    reactivate()
else:
    print False

What do you think?


Some additional comments:

  • that code is a sample above, it works, but it could probably do with some better matching to the rest of your code and conventions, it is just a proof-of-concept that is proven to work:
  • in your main Dockerfile, you can just install python-dbus instead of the current manual install, and that saves about 35MB on your image, just FYI:
RUN apt-get update && apt-get install -y \
	python-dbus \
	&& rm -rf /var/lib/apt/lists/
1 Like

@imrehg This looks good to me. I am bouncing a bit between some other projects today but I will try this out as soon as I am able and report back.

This does appear to have worked for me. Gonna run some more tests. But so far, initial tests are working

Glad to hear. :tada:
The NetworkManager DBUS interface seems really hairy (or any DBUS interface I came across so far:), but it can be very capable, can recommend digging into their docs to see any further options if needed for your networking.

So far every test has passed. So Im fine closing this up.

Just wanted to thank you both @mccollam & @imrehg for bearing with me and helping us through this. Much appreciated.

1 Like

Sure thing, let us know if there are any other topics we can help with. Also, would love to hear how your project works out, it’s always great to see what people are building on resin, very inspirational! :smiley: