Finishing the VPN Client app's functionality
-
So, I put a function in
docker.js
:function getVPNContainerName() { var containerIdVPN; gConnection.getNetwork('cloudron').inspect(function (error, bridge) { for (var id in bridge.Containers) { let v = inspect (id, function (error, container) { if(typeof container === "object") { var environment = safe.query(container, 'Config.Env', null); if (JSON.stringify(environment).includes("openvpnclient")) { containerIdVPN = container.Name.substring(1); debug ("INSIDE INSIDE FUNCTION:" + containerIdVPN); return containerIdVPN; //if (!containerIdVPN) return callback(new BoxError(BoxError.DOCKER_ERROR, 'No VPN Client app to connect to')); } } }); } }); }
And then I changed one line (https://git.cloudron.io/cloudron/box/-/blob/master/src/docker.js#L323) to run my function
containerOptions.HostConfig.NetworkMode = 'container:' + getVPNContainerName();
And because Node is asynchronous, It NEVER actually finishes the function (thus the "container" is undefined). I've tried putting the function all kinds of places. But it still runs after it's needed. Like, I need the VPN container name to attach it to the NetworkMode but it runs after the function is written? I tried using
Promises
but that didn't work. I knowbox
is written in all these callback methodologies but I've only written in synchronous languages so I don't know how to get the VPN name before it's needed, using that function (which works perfectly). -
Totally figured it out. In the absolutely wrong way (sure @girish would hate it) but it works!
Right now, it's just hardcoded to connect to the VPN Client if another app has "vpnconnect" anywhere in their domain name. But this is just a proof of concept. It's horribly ineffecient. I don't know enough about
box
and how asynchronousity works, see, I can't even spell it.function createSubcontainer(app, name, cmd, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof name, 'string'); assert(!cmd || util.isArray(cmd)); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); let isAppContainer = !cmd; // non app-containers are like scheduler var manifest = app.manifest; var exposedPorts = {}, dockerPortBindings = {}; var domain = app.fqdn; const envPrefix = manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_'; let stdEnv = [ 'CLOUDRON=1', 'CLOUDRON_PROXY_IP=172.18.0.1', `CLOUDRON_APP_HOSTNAME=${app.id}`, `${envPrefix}WEBADMIN_ORIGIN=${settings.adminOrigin()}`, `${envPrefix}API_ORIGIN=${settings.adminOrigin()}`, `${envPrefix}APP_ORIGIN=https://${domain}`, `${envPrefix}APP_DOMAIN=${domain}` ]; // docker portBindings requires ports to be exposed exposedPorts[manifest.httpPort + '/tcp'] = {}; dockerPortBindings[manifest.httpPort + '/tcp'] = [{ HostIp: '127.0.0.1', HostPort: app.httpPort + '' }]; var portEnv = []; for (let portName in app.portBindings) { const hostPort = app.portBindings[portName]; const portType = (manifest.tcpPorts && portName in manifest.tcpPorts) ? 'tcp' : 'udp'; const ports = portType == 'tcp' ? manifest.tcpPorts : manifest.udpPorts; var containerPort = ports[portName].containerPort || hostPort; exposedPorts[`${containerPort}/${portType}`] = {}; portEnv.push(`${portName}=${hostPort}`); dockerPortBindings[`${containerPort}/${portType}`] = [{ HostIp: '0.0.0.0', HostPort: hostPort + '' }]; } let appEnv = []; Object.keys(app.env).forEach(function(name) { appEnv.push(`${name}=${app.env[name]}`); }); // first check db record, then manifest var memoryLimit = app.memoryLimit || manifest.memoryLimit || 0; if (memoryLimit === -1) { // unrestricted memoryLimit = 0; } else if (memoryLimit === 0 || memoryLimit < constants.DEFAULT_MEMORY_LIMIT) { // ensure we never go below minimum (in case we change the default) memoryLimit = constants.DEFAULT_MEMORY_LIMIT; } // give scheduler tasks twice the memory limit since background jobs take more memory // if required, we can make this a manifest and runtime argument later if (!isAppContainer) memoryLimit *= 2; addons.getEnvironment(app, function(error, addonEnv) { if (error) return callback(error); var containerOptions = { name: name, // for referencing containers Tty: isAppContainer, Image: app.manifest.dockerImage, Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd, Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv), ExposedPorts: isAppContainer ? exposedPorts : {}, Volumes: { // see also ReadonlyRootfs '/tmp': {}, '/run': {} }, Labels: { 'fqdn': app.fqdn, 'appId': app.id, 'isSubcontainer': String(!isAppContainer), 'isCloudronManaged': String(true) }, HostConfig: { Mounts: addons.getMountsSync(app, app.manifest.addons), Binds: getBindsSync(app), // ideally, we have to use 'Mounts' but we have to create volumes then LogConfig: { Type: 'syslog', Config: { 'tag': app.id, 'syslog-address': 'udp://127.0.0.1:2514', // see apps.js:validatePortBindings() 'syslog-format': 'rfc5424' } }, Memory: memoryLimit / 2, MemorySwap: memoryLimit, // Memory + Swap PortBindings: isAppContainer ? dockerPortBindings : {}, PublishAllPorts: false, ReadonlyRootfs: app.debugMode ? !!app.debugMode.readonlyRootfs : true, RestartPolicy: { 'Name': isAppContainer ? 'unless-stopped' : 'no', 'MaximumRetryCount': 0 }, CpuShares: app.cpuShares, VolumesFrom: isAppContainer ? null : [app.containerId + ':rw'], SecurityOpt: ['apparmor=docker-cloudron-app'], CapAdd: [], CapDrop: [] } }; // do no set hostname of containers to location as it might conflict with addons names. for example, an app installed in mail // location may not reach mail container anymore by DNS. We cannot set hostname to fqdn either as that sets up the dns // name to look up the internal docker ip. this makes curl from within container fail // Note that Hostname has no effect on DNS. We have to use the --net-alias for dns. // This is done to prevent lots of up/down events and iptables locking if (isAppContainer) { if (app.fqdn.includes('vpnconnect')) { getVPNContainerName(function(err, val) { if (err) { callback(new BoxError(BoxError.ALREADY_EXISTS, error)); } else { containerOptions.name = app.id; containerOptions.Hostname = ''; containerOptions.ExposedPorts = {}; containerOptions.HostConfig.PortBindings = {}; containerOptions.HostConfig.NetworkMode = "container:" + val; finish(); } }); } else { containerOptions.Hostname = app.id; containerOptions.HostConfig.NetworkMode = 'cloudron'; // user defined bridge network containerOptions.HostConfig.Dns = ['172.18.0.1']; // use internal dns containerOptions.HostConfig.DnsSearch = ['.']; // use internal dns containerOptions.NetworkingConfig = { EndpointsConfig: { cloudron: { Aliases: [name] // adds hostname entry with container name } } }; debug ("Create container" + app.id); finish(); } } else { containerOptions.Hostname = app.id; containerOptions.HostConfig.NetworkMode = 'container:${app.containerId}'; // container defined bridge network containerOptions.HostConfig.Dns = ['172.18.0.1']; // use internal dns containerOptions.HostConfig.DnsSearch = ['.']; // use internal dns debug ("Create container" + app.id); finish(); } // asynchronous function, calls callback(err, val) when it's complete function getVPNContainerName(callback) { var containerIdVPN; var done = false; gConnection.getNetwork('cloudron').inspect(function(error, bridge) { if (error) { callback(error); return; } for (var id in bridge.Containers) { // problem with sequencing of this async operation inside of for loop let v = inspect(id, function(error, container) { // asynchronous function, calls callback(err, val) when it's complete function getVPNContainerName(callback) { var containerIdVPN; var done = false; gConnection.getNetwork('cloudron').inspect(function(error, bridge) { if (error) { callback(error); return; } for (var id in bridge.Containers) { // problem with sequencing of this async operation inside of for loop let v = inspect(id, function(error, container) { if (error) { // log error and skip debug(error); return; } if (typeof container === "object") { var environment = safe.query(container, 'Config.Env', null); if (JSON.stringify(environment).includes("openvpnclient")) { containerIdVPN = container.Name.substring(1); debug("INSIDE INSIDE FUNCTION:" + containerIdVPN); if (!done) { done = true; callback(null, containerIdVPN); } } } }); } if (!done) { // use default value } }); } function finish() { var capabilities = manifest.capabilities || []; // https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities if (capabilities.includes('net_admin')) containerOptions.HostConfig.CapAdd.push('NET_ADMIN', 'NET_RAW'); if (capabilities.includes('mlock')) containerOptions.HostConfig.CapAdd.push('IPC_LOCK'); // mlock prevents swapping if (!capabilities.includes('ping')) containerOptions.HostConfig.CapDrop.push('NET_RAW'); // NET_RAW is included by default by Docker if (capabilities.includes('vaapi') && safe.fs.existsSync('/dev/dri')) { containerOptions.HostConfig.Devices = [ { PathOnHost: '/dev/dri', PathInContainer: '/dev/dri', CgroupPermissions: 'rwm' } ]; } containerOptions = _.extend(containerOptions, options); gConnection.createContainer(containerOptions, function(error, container) { if (error && error.statusCode === 409) return callback(new BoxError(BoxError.ALREADY_EXISTS, error)); if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); callback(null, container); }); } }); }
-
I could just lock the files (docker.js, and reverseproxy.js I had to make changes to) from updates so I can keep my VPN connection between apps in tact, but that seems a bit harsh. I just feel like I don't know enough about node or
box
to code this "right". -
Since, async functions can get some time to get used to, what you can do as a "hack" for now is to just set a property in appdb. For example, add a line https://git.cloudron.io/cloudron/box/-/blob/master/src/appdb.js#L59 like
result.networkId = container:vpncontainerappid
. Then you have this properly available in the docker.js code.I think the final code will anyway read the networkId from the app table, so your code won't require much change if you do something like above.
-
@girish said in Finishing the VPN Client app's functionality:
Since, async functions can get some time to get used to, what you can do as a "hack" for now is to just set a property in appdb. For example, add a line https://git.cloudron.io/cloudron/box/-/blob/master/src/appdb.js#L59 like
result.networkId = container:vpncontainerappid
. Then you have this properly available in the docker.js code.I think the final code will anyway read the networkId from the app table, so your code won't require much change if you do something like above.
That’s perfect. I hated how hacky my solution was so I’m going to to recode it in the way you described. Doing it the way you described also allows me to add a comma-delimited list of all VPN containers thus supporting more than one VPN client running simultaneously on Cloudron (my hacky code only allows for a single OpenVPN client).
I need to add the container name to the DB and the internal IP of the running vpn client container. Both are essential.
Then when any user chooses to connect to the VPN then that specific app will restart and be configured to route all outgoing traffic to it.
The only caveat in all of this is that though technically an unlimited amount of web apps can connect to the vpn client, a limitation is that the web apps can’t share the same exposed port as any of the others. This is gotten around in Docker by binding randomized ports to the exposed port. But when you connect to a VPN Client, you have to be running on the same IP so you lose the ability to Docker “bind.”
There may be a way around this using Docker Connect
-link
argument. But I think the same caveat would apply and that would only solve the problem of having the app not have to do a quick restart to connect. But I’ll make sure. ️