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 424 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