Skip to content

OpenVPN Client Export #368

Closed
Closed

Description

@VirtualSamuraii

Hi there, thank you for your amazing work with this API.

As i'm trying to automate the process of creating individual access to a VPN server, I have developed a PHP script that creates a user certificate using the API endpoints System > Certificates.

Is there any available endpoint allowing me to export the vpn client config using this certificate ?

Activity

  1. mrizkihidayat66 commented on Dec 18, 2024

    @mrizkihidayat66

    usr\local\pkg\RESTAPI\Endpoints\VPNOpenVPNExportsEndpoint.inc

    <?php
    
    namespace RESTAPI\Endpoints;
    
    require_once 'RESTAPI/autoloader.inc';
    
    use RESTAPI\Core\Endpoint;
    
    /**
     * Defines an Endpoint for interacting with a single OpenVPNExport Model object at
     * /api/v2/vpn/openvpn/exports.
     */
    class VPNOpenVPNExportsEndpoint extends Endpoint {
        public function __construct() {
            # Set Endpoint attributes
            $this->url = '/api/v2/vpn/openvpn/exports';
            $this->model_name = 'OpenVPNExport';
            $this->request_method_options = ['GET'];
            $this->many = true;
    
            # Construct the parent Endpoint object
            parent::__construct();
        }
    }

    usr\local\pkg\RESTAPI\Endpoints\VPNOpenVPNExportEndpoint.inc

    <?php
    
    namespace RESTAPI\Endpoints;
    
    require_once 'RESTAPI/autoloader.inc';
    
    use RESTAPI\Core\Endpoint;
    
    /**
     * Defines an Endpoint for interacting with a single OpenVPNExport Model object at
     * /api/v2/vpn/openvpn/export.
     */
    class VPNOpenVPNExportEndpoint extends Endpoint {
        public function __construct() {
            # Set Endpoint attributes
            $this->url = '/api/v2/vpn/openvpn/export';
            $this->model_name = 'OpenVPNExport';
            $this->request_method_options = ['GET', 'PATCH'];
    
            # Construct the parent Endpoint object
            parent::__construct();
        }
    }

    usr\local\pkg\RESTAPI\Models\OpenVPNExport.inc

    <?php
    
    namespace RESTAPI\Models;
    
    use RESTAPI\Core\Model;
    use RESTAPI\Fields\BooleanField;
    use RESTAPI\Fields\ForeignModelField;
    use RESTAPI\Fields\IntegerField;
    use RESTAPI\Fields\PortField;
    use RESTAPI\Fields\ObjectField;
    use RESTAPI\Fields\StringField;
    use RESTAPI\Responses\ConflictError;
    use RESTAPI\Responses\ValidationError;
    use RESTAPI\Validators\HostnameValidator;
    use RESTAPI\Validators\IPAddressValidator;
    
    /**
     * Defines a Model that represents OpenVPN Export.
     */
    class OpenVPNExport extends Model {
        const DMYPWD = "********";
    
        //public ForeignModelField $server;
        public IntegerField $server;
        public StringField $useaddr;
        public StringField $useaddr_hostname;
        public StringField $verifyservercn;
        public BooleanField $blockoutsidedns;
        public BooleanField $legacy;
        public BooleanField $silent;
        public StringField $bindmode;
        # SKIP: public BooleanField $usepkcs11;
        public BooleanField $usetoken;
        public BooleanField $usepass;
        public StringField $pass;
        public StringField $p12encryption;
        public BooleanField $useproxy;
        public StringField $useproxytype;
        public StringField $proxyaddr;
        public PortField $proxyport;
        public StringField $useproxypass;
        public StringField $proxyuser;
        public StringField $proxypass;
        public StringField $advancedoptions;
        public ObjectField $users;
    
        public function __construct(mixed $id = null, mixed $parent_id = null, mixed $data = [], mixed ...$options) {
            # Set model attributes
            $this->config_path = 'installedpackages/vpn_openvpn_export/serverconfig/item';
            $this->packages = ['pfSense-pkg-openvpn-client-export'];
            $this->package_includes = ['openvpn-client-export.inc'];
            $this->always_apply = true;
            $this->many = true;
    
            # Set model fields
            //$this->server = new ForeignModelField(
            //    model_name: 'OpenVPNServer',
            //    model_field: 'vpnid',
            //    default: null,
            //    allow_empty: true,
            //    allow_null: true,
            //    many: true,
            //    help_text: 'The VPN ID of the OpenVPN server this client export corresponds to.',
            //);
            $this->server = new IntegerField(
                unique: true,
                default_callable: 'get_next_vpn_id',
                editable: true,
                read_only: false,
                help_text: 'The unique ID for this OpenVPN server. This value is assigned by the ' .
                    'system and cannot be changed.',
            );
    
            # Client Connection Behavior
            $this->useaddr = new StringField(
                required: true,
                choices: ['serveraddr', 'servermagic', 'servermagichost', 'serverhostname', 'other'],
                help_text: 'Host Name Resolution'
            );
            $this->useaddr_hostname = new StringField(
                required: true,
                allow_empty: false,
                conditions: ['useaddr' => 'other'],
                validators: [new HostnameValidator(allow_hostname: true, allow_domain: true, allow_fqdn: true)],
                help_text: 'Host Name'
            );
            $this->verifyservercn = new StringField(
                default: 'auto',
                choices: ['auto', 'none'],
                help_text: 'Verify Server CN'
            );
            $this->blockoutsidedns = new BooleanField(
                default: false,
                indicates_true: 'yes',
                indicates_false: '',
                help_text: 'Block Outside DNS'
            );
            $this->legacy = new BooleanField(
                default: false,
                indicates_true: 'yes',
                indicates_false: '',
                help_text: 'Legacy Client'
            );
            $this->silent = new BooleanField(
                default: false,
                indicates_true: 'yes',
                indicates_false: '',
                help_text: 'Silent Installer'
            );
            $this->bindmode = new StringField(
                default: 'nobind',
                choices: ['nobind', 'lport0', 'bind'],
                help_text: 'Bind Mode'
            );
    
            # Certificate Export Options
            $this->usetoken = new BooleanField(
                default: false,
                indicates_true: 'yes',
                indicates_false: '',
                help_text: 'Microsoft Certificate Storage'
            );
            $this->usepass = new BooleanField(
                default: false,
                indicates_true: 'yes',
                indicates_false: '',
                help_text: 'Password Protect Certificate'
            );
            $this->pass = new StringField(
                required: true,
                default: null,
                conditions: ['usepass' => true],
                help_text: 'Certificate Password'
            );
            $this->p12encryption = new StringField(
                default: 'high',
                choices: ['high', 'low', 'legacy'],
                help_text: 'PKCS#12 Encryption'
            );
    
            # Proxy Options
            $this->useproxy = new BooleanField(
                default: false,
                indicates_true: 'yes',
                indicates_false: '',
                help_text: 'Use A Proxy'
            );
            $this->useproxytype = new StringField(
                default: 'http',
                choices: ['http', 'socks'],
                conditions: ['useproxy' => true],
                help_text: 'Proxy Type'
            );
            $this->proxyaddr = new StringField(
                required: true,
                allow_empty: false,
                default: null,
                validators: [new IPAddressValidator(allow_ipv4: true, allow_ipv6: false)],
                conditions: ['useproxy' => true],
                help_text: 'Proxy IP Address',
            );
            $this->proxyport = new PortField(
                required: true,
                unique: true,
                default: null,
                allow_alias: false,
                allow_range: false,
                conditions: ['useproxy' => true],
                help_text: 'Proxy Port',
            );
            $this->useproxypass = new StringField(
                required: true,
                default: null,
                choices: ['none', 'basic', 'ntlm'],
                conditions: ['useproxy' => true],
                help_text: 'Proxy Authentication'
            );
            $this->proxyuser = new StringField(
                required: true,
                default: null,
                conditions: ['useproxy' => true, 'useproxypass' => ['basic', 'ntlm']],
                help_text: 'Proxy Username'
            );
            $this->proxypass = new StringField(
                required: true,
                conditions: ['useproxy' => true, 'useproxypass' => ['basic', 'ntlm']],
                help_text: 'Proxy Password'
            );
    
            # Advanced
            $this->advancedoptions = new StringField(
                default: [],
                allow_empty: true,
                many: true,
                delimiter: ';',
                help_text: 'Additional options to add to the OpenVPN client export configuration.',
            );
    
            # OpenVPN Clients
            $this->users = new ObjectField(
                default_callable: 'get_user_certificate',
                allow_empty: true,
                many: true,
                many_minimum: 0,
                help_text: '',
            );
    
            parent::__construct($id, $parent_id, $data, ...$options);
        }
    
        /**
         * Obtains the next available VPN ID.
         * @return integer The next available ID number.
         */
        public function get_next_vpn_id(): int {
            return openvpn_vpnid_next();
        }
    
        /**
         * 
         */
        public function get_user_certificate(): array {
            $users = [];
            foreach (User::read_all()->model_objects as $user) {
                $users[] = $user;
            }
            return $users;
        }
    
        /**
         * Applies changes to this OpenVPN Server.
         */
        public function apply(): void {
            
        }
    }

    Hi @jaredhendrickson13 can you help me with this case, I'm trying to create this endpoint but not works with 404 Not Found error :'(

  2. jaredhendrickson13 commented on Dec 18, 2024

    @jaredhendrickson13
    Member

    @mrizkihidayat66 once you have the endpoint class written to /usr/local/pkg/RESTAPI/Endpoints/ you'll need to run pfsense-restapi buildendponts to generate the endpoint in the pfSense web root.

    You may find these resources useful for building out models and endpoints as well:

    https://pfrest.org/BUILDING_CUSTOM_MODEL_CLASSES/

    https://pfrest.org/BUILDING_CUSTOM_ENDPOINT_CLASSES/

  3. mrizkihidayat66 commented on Dec 19, 2024

    @mrizkihidayat66

    @jaredhendrickson13 Hi, thank you. I can now make the API respond, but I still can't get it to return what I expect. Can you help me? I want to be able to download an OpenVPN client configuration file from the API.

    image

  4. Ceald1 commented on Jan 25, 2025

    @Ceald1

    Any updates on this? I also can't figure out how to export the ovpn file from the API?

  5. cvanbrummelen commented on May 13, 2025

    @cvanbrummelen

    Would love to have this added aswell.

  6. CyberHippo commented on Jun 5, 2025

    @CyberHippo

    That would be a great feature!

  7. cvanbrummelen commented on Jul 8, 2025

    @cvanbrummelen

    Any luck on getting it to work @mrizkihidayat66 ?

  8. jaredhendrickson13 commented on Jul 8, 2025

    @jaredhendrickson13
    Member

    The majority of the work for this feature is done and currently in next_minor. Still needs additional tests written and some quirkiness with some of the required pfSense functions to be fully ironed out. Assuming all that goes well, it should be included in v2.6.0.

  9. cvanbrummelen commented on Jul 10, 2025

    @cvanbrummelen

    Greate news!

  10. jaredhendrickson13 commented on Jul 26, 2025

    @jaredhendrickson13
    Member

    Dev build v2.6.0-dev-772a828 has this feature implemented and tested for anyone looking to test their own implementations early or get any last minute bugs worked out.

    A stable v2.6.0 release likely won't be until shortly after pfSense CE 2.8.1 and pfSense Plus 25.07 are released, so still likely a couple weeks out at least.

  11. linked a pull request that will close this issuev2.6.0 Features & Fixes #747on Sep 5, 2025
  12. cvanbrummelen commented on Sep 8, 2025

    @cvanbrummelen

    I might be doing something wrong but when I call vpn/openvpn/client_export/configs
    The return is a 200 code but the data is empty.

    Array ( [code] => 200 [status] => ok [response_id] => SUCCESS [message] => [data] => Array ( ) )

    This should return all client exports available on the VPN > OpenVPN > Client Export tab right?

  13. jaredhendrickson13 commented on Sep 9, 2025

    @jaredhendrickson13
    Member

    @cvanbrummelen Yes, but it is a two step process. The GET /api/v2/vpn/client_export/configs endpoint lists the client export configuration settings you have saved for your OpenVPN servers. If you haven't yet saved an export configuration using the 'Save as default' UI button, or via POST /api/v2/vpn/openvpn/client_export/config, it would be expected that the data list is empty. This process is the API equivalent of reading/saving the main settings on the Client Export page:

    Image

    Once a configuration is saved, you use its id to generate the actual client installation file. This is done with the POST /api/v2/vpn/openvpn/client_export endpoint, which is the equivalent of using the download links at the bottom of the page:

    Image
  14. cvanbrummelen commented on Sep 9, 2025

    @cvanbrummelen

    @jaredhendrickson13 this does push me in the right direction but Im still missing something I think.
    I now saved the default config and doing a post to vpn/openvpn/client_export gives more.

    However, when I send over the id, type, certref and username I get the error: The OpenVPN client export failed for the following reason: Could not find client certificate.

    However If I check the username and refid they are correct.

  15. cvanbrummelen commented on Sep 9, 2025

    @cvanbrummelen

    I did some further debugging and it seems the code from openvpn-client-export.inc (from pfSense itself) is returning the wrong thing.

    In this line "$cert = $user['cert'][$crtid];" the $crtid is 30. However the $user array/object returning has the cert with id 30. The certificate itself is ID 30 but inside the $user object/array it has a key value of 0.

    When I hardcore the $crtid to 0 it does return the configs.

  16. cvanbrummelen commented on Sep 22, 2025

    @cvanbrummelen

    Any info about this @jaredhendrickson13 ?

  17. jaredhendrickson13 commented on Sep 23, 2025

    @jaredhendrickson13
    Member

    @cvanbrummelen I'll probably need more info to say for sure. Go ahead and open a new bug report for that issue so I can keep better track of it. In that report, if you can include examples of the requests you're using to reproduce the issue along with the user, certificate, OpenVPN server, and OpenVPN client export config sections from your XML configuration (feel free to obfuscate sensitive data), that will help me determine exactly what's happening and the best way to fix it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions