Cloudron makes it easy to run web apps like WordPress, Nextcloud, GitLab on your server. Find out more or install now.


Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Bookmarks
  • Search
Skins
  • Light
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Default (No Skin)
  • No Skin
Collapse
Brand Logo

Cloudron Forum

Apps | Demo | Docs | Install
  1. Cloudron Forum
  2. App Packaging & Development
  3. Finishing the VPN Client app's functionality

Finishing the VPN Client app's functionality

Scheduled Pinned Locked Moved App Packaging & Development
5 Posts 2 Posters 401 Views 2 Watching
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • LonkleL Offline
      LonkleL Offline
      Lonkle
      wrote on last edited by Lonkle
      #1

      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 know box 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).

      1 Reply Last reply
      0
      • LonkleL Offline
        LonkleL Offline
        Lonkle
        wrote on last edited by Lonkle
        #2

        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);
                    });
                }
            });
        }
        
        1 Reply Last reply
        0
        • LonkleL Offline
          LonkleL Offline
          Lonkle
          wrote on last edited by Lonkle
          #3

          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".

          1 Reply Last reply
          0
          • girishG Offline
            girishG Offline
            girish
            Staff
            wrote on last edited by
            #4

            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.

            LonkleL 1 Reply Last reply
            0
            • girishG girish

              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.

              LonkleL Offline
              LonkleL Offline
              Lonkle
              wrote on last edited by
              #5

              @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. ☺️

              1 Reply Last reply
              0
              Reply
              • Reply as topic
              Log in to reply
              • Oldest to Newest
              • Newest to Oldest
              • Most Votes


                • Login

                • Don't have an account? Register

                • Login or register to search.
                • First post
                  Last post
                0
                • Categories
                • Recent
                • Tags
                • Popular
                • Bookmarks
                • Search