OpenVPN OTP Authentication

Hey Erik,
I just had a look in the package and i would need the libpam-google-authenticator for otp authentication. Would it be possible to build that?

Greetings

Steffen

Hi Steffen,
i think the name differs on distribution. In here --> https://github.com/google/google-authenticator-libpam/issues/116 e.g. the .so calls also ‘pam_google_authenticator.so’ … Compiled it like described in here --> https://github.com/google/google-authenticator/wiki/PAM-Module-Instructions --> https://velenux.wordpress.com/2019/03/12/openvpn-with-google-2-factor-authentication-on-centos-7/ and get the .so in that name. If you have other building instructions/howtos just post them.
Have nevertheless found a newer version 1.08, thinking also about to add an own group/user for google-authenticator like in here --> https://medium.com/we-have-all-been-there/using-google-authenticator-mfa-with-openvpn-on-ubuntu-16-04-774e4acc2852 .

Some other ideas are:

A downside to the OTP authentication is what i have seen so far, the reneg-sec 3600 (rekeying) was mostly disabled.

Some beneath infos.

Best,

Erik

If you use the compiling instruction on github everything should be fine. I had a look on the medium how to as well. I think using the openvpn otp plugin could be a nice idea too.

google-authenticator-1.08 , openvpn-otp-1.0 and libqrencode-4.0.0 are now ready and can be found in here --> https://people.ipfire.org/~ummeegge/otp/ .

In- and unstallation can be made via the scripts in the package. Copy it to /opt/pakfire/tmp and execute them.

Made a faster test with google-authenticator only which looked OK on the first view but there seems a problem with the PAM modul.

First Steps:

  • Created a user with own directory under home.
  • su ed into it and did execute
bash-4.3$ google-authenticator -C -t -f -D -r 3 -Q UTF8 -R 30 -w3 -s /home/testotp/google-authenticator/testotp

which results in

Warning: pasting the following URL into your browser exposes the OTP secret to Google:
  https://www.google.com/chart?chs=200x200&chld=M|0&cht=qr&chl=otpauth://totp/testotp@ipfire-janeisklar.local%3Fsecret%3DD7MJSHIK3OWMT27X2APJAR3RUU%26issuer%3Dipfire-janeisklar.local
                                             
  █▀▀▀▀▀█ █▄▄▀▀  ▀▄ ▀▄▀▄▄▀ █  ▀ ██  █▀▀▀▀▀█  
  █ ███ █ ▀█▄ █▄▄▀  █▀▀█▀ ██▀██▄  █ █ ███ █  
  █ ▀▀▀ █ ▀▀██▄█ ▄ ██▀▀▀ ▄    ▀  ▀  █ ▀▀▀ █  
  ▀▀▀▀▀▀▀ ▀ █ ▀ ▀▄▀ █▄▀ █▄█▄█▄█ ▀ ▀ ▀▀▀▀▀▀▀  
  ▀▄▄█▀▀▀█▀▄▄  ▀▄ ▄█▄▀  ▄▄▀ ▄▄ ▀██▄▀▄▄█▄▀█▀  
   █ ▄▀▄▀ ▄▄ █ █▀ ▀█  ▄▀▀█ ▀█▀ █ ▄▀▄█  █ ▄   
  ▄▀▄▄██▀██▀ ▀▄▀▄█▄▀▄▀▀  ▄█▄▄██▄  ▀ █▄▄▄ ▀▀  
  █▄█  ▄▀▄█▀█▀▀▄  ▄▀▄▀█ █▄█ ▄█▄  █ ▄██▀▄▄▄█  
  █▄█▀▀ ▀█▄█ █▄█▀█  ▄ ▄▄ █▀  ▀▀█  ▄▄  ▄▀▄ ▀  
   ▄ ▀ █▀   █ ██▄▀█▄▄█ ▀ ██▄██▄ █▀ ██ █▀▄ █  
  ▄ ▀▄█ ▀█ ▄▀ ▄ ▀ █▄▄█▀ ▄█ ▄▀█▀█▄▄▀ ▀▀▄▄▄ ▀  
   █  ▄▀▀▀ ▄███ █▀▀█▀ ▄▀▀▀ ▀▄▀ ▄ ▄▀ ▀█▄█▀ ▄  
  ▀█▄▄▄▄▀ ▄▀▄   █ ▄  ▀▀  ▄▀▄ ▄▄  ▀▄████▀▄█▄  
   ▄ ▀▄▄▀ ▀ █ ▄▄ ▀█ ▄█▀ ▀  ▀ █ ▀█▄█▄▀▄█▄▄▀  
  █ ▀  ▀▀█▀▀ ▄█▄▀▄  ▀▄█ ▀  ▀ ▄▀▄███▄ █▄▀ ▀▄  
  ██  █▄▀█▄▀▄ ██ ▀▀▀▀▄ █▀ █▀▀▄▀▄██ ▀▄█▀▄▄█  
  ▀▀ ▀ ▀▀▀█▄▀ ▀██ ▀▀ ▀▀█  ▀▀▀▄▄▀▀▀█▀▀▀█▀█ ▀  
  █▀▀▀▀█ ██▀▄▀▄█▀ █▀▄▀ ▀██▀ █▄█ ▄█ ▀ ██  ▄  
  █ ███ █ ███▀▄▄▄█▄█▀▀▀  █▄▄▄██ █▀▀█▀▀▀▀█▀█  
  █ ▀▀▀ █   █▀ ▄  ▄▀ █▄   ▀▀▀█▀ ▄█ █▄▀█▄▄ █  
  ▀▀▀▀▀▀▀ ▀▀▀▀     ▀  ▀ ▀ ▀▀     ▀▀  ▀▀▀     
                                             
Your new secret key is: SGHJSCON3OSHJ27X2APJAR3RUU
Your verification code for code 1 is 047584
Your emergency scratch codes are:
  27565487
  82723306
  34445652
  48456451
  54560420

checked bar code with an iPhone and google authenticator from app store which worked.

  • Added new PAM profile called ‘openvpn’ with the following content
auth required           /usr/lib/security/pam_google_authenticator.so secret=/home/testotp/google-authenticator/testotp forward_pass debug
  • Used “Additional configuration” for server and client configuration on OpenVPN with
# Google authenticator
plugin /usr/lib/openvpn/plugins/openvpn-plugin-auth-pam.so openvpn

for server.conf.local and

# Google-authenticator
# use username/password authentication
auth-user-pass
# do not cache auth info
auth-nocache

for client.conf.local

But stucked at:

Mar  9 10:12:12 ipfire openvpn[17716]: PAM _pam_init_handlers: no default config other
Mar  9 10:12:12 ipfire openvpn(pam_google_authenticator)[17716]: debug: start of google_authenticator for "testotp"
Mar  9 10:12:12 ipfire openvpn(pam_google_authenticator)[17716]: debug: Secret file permissions are 0600. Allowed permissions are 0600
Mar  9 10:12:12 ipfire openvpn(pam_google_authenticator)[17716]: debug: "/home/testotp/google-authenticator/testotp" read
Mar  9 10:12:12 ipfire openvpn(pam_google_authenticator)[17716]: debug: shared secret in "/home/testotp/google-authenticator/testotp" processed
Mar  9 10:12:12 ipfire openvpn(pam_google_authenticator)[17716]: debug: google_authenticator for host "(null)"
Mar  9 10:12:12 ipfire openvpn(pam_google_authenticator)[17716]: debug: no scratch code used from "/home/testotp/google-authenticator/testotp"
Mar  9 10:12:12 ipfire openvpn(pam_google_authenticator)[17716]: Accepted google_authenticator for testotp
Mar  9 10:12:12 ipfire openvpn(pam_google_authenticator)[17716]: debug: "/home/testotp/google-authenticator/testotp" written
Mar  9 10:12:12 ipfire openvpn(pam_google_authenticator)[17716]: debug: end of google_authenticator for "testotp". Result: Success
Mar  9 10:12:12 ipfire openvpn[17716]: PAM no modules loaded for 'openvpn' service
Mar  9 10:12:12 ipfire openvpnserver[17715]: 192.168.90.4:40087 PLUGIN_CALL: POST /usr/lib/openvpn/plugins/openvpn-plugin-auth-pam.so/PLUGIN_AUTH_USER_PASS_VERIFY status=1
Mar  9 10:12:12 ipfire openvpnserver[17715]: 192.168.90.4:40087 PLUGIN_CALL: plugin function PLUGIN_AUTH_USER_PASS_VERIFY failed with status 1: /usr/lib/openvpn/plugins/openvpn-plugin-auth-pam.so
Mar  9 10:12:12 ipfire openvpnserver[17715]: 192.168.90.4:40087 TLS Auth Error: Auth Username/Password verification failed for peer

Am currently a little short in time. May you find some more out.

Best,

Erik

1 Like

Yes sure i will have a look thanks for your work :smiley:

Hey Erik,

after some hours I got it to work. I configured the OpenVPN-Server to use TOTP in connection with username/password autentication using pam_unix.so

I first configured the OpenVPN-Server to use authentication via username/password using this how-to: https://wiki.ipfire.org/configuration/services/openvpn/extensions/plugins/auth-pam

After that was working i started to integrate OTP authentication.

I created a folder where the secret files of the users should be stored and created a user for creating the software tokens:

addgroup gauth
useradd -g gauth gauth
mkdir /var/ipfire/ovpn/google-authenticator
chown gauth:gauth /var/ipfire/ovpn/google-authenticator
chmod 0700 /var/ipfire/ovpn/google-authenticator

I customized the /etc/pam.d/openvpn :

#Google Authenticator
auth    requisite       /usr/lib/security/pam_google_authenticator.so secret=/var/ipfire/ovpn/google-authenticator/${USER} user=gauth forward_pass debug

#Username/Password authentication
auth    required        pam_unix.so use_first_pass
account required        pam_unix.so

This is what my server.conf.local looks like:
plugin /usr/lib/openvpn/plugins/openvpn-plugin-auth-pam.so /etc/pam.d/openvpn

Here are my entrys in client.conf.local:

#Username - Password Authentication
auth-user-pass

#Do not cache auth info
auth-nocache

For creating new users more convenient I use the following shellscript:
#!/bin/bash

#variables

#MFA Label
MFA_LABEL='Testcorp OpenVPN-Server'

#MFA User
MFA_USER=gauth

#Directory for Secretfiles
MFA_DIR=/var/ipfire/ovpn/google-authenticator


##########################################################################
echo -en "Please enter new username:"
read user_id

if [ "$user_id" = "" ]; then
	echo "ERROR: No username specified"
	exit 1
fi

echo "Creating account ${user_id}"
useradd -s /bin/false "$user_id"

echo "Please enter password for new user"
passwd "$user_id"

echo "Creating MFA token"
su -c "google-authenticator -t -C -d -r3 -R30 -f -l \"${MFA_LABEL}\" -s $MFA_DIR/${user_id}" - $MFA_USER | tee $MFA_DIR/otp-config/$user_id

For logging in using the OpenVPN client i created roadwarrior connection for each user over the WUI
and did the import on the client machine. The credentials you have to type in when connecting are the following:

username : USER_ID
password: password+otp-token (for example: password934741)

Hope I didn’t miss something. I hope the packages could find a way into the pakfire repo :slight_smile:

Greetings

Steffen

1 Like

Great work Steffen! Will give it a try may in the evening.
Have had the user/group/dir and permissions block also in the install.sh of google-authenticator. Am thinking about to reintegrate it or should we keep it simple ?
Do you think we can bring other additionals to the package ?
May the PAM config might be nice ?
Should we rework the script a little to integrate it also into the package ?

Best,

Erik

Hey Erik,

I think it would be useful to integrate it into the install script to keep it simple for the user. I would also integrate the pam profile and the outputfolders /var/ipfire/ovpn/google-authenticator (for the secret files) and /var/ipfire/ovpn/google-authenticator/otp-config (for the setup and recovery information for each token) and the needed permissions.

Greetings

Steffen

Hi Steffen,
did test it and what should i say, i works :slightly_smiling_face:

Mar 11 14:53:00 ipfire openvpn[5654]: PAM _pam_init_handlers: no default config other
Mar 11 14:53:00 ipfire google-auth-openvpn(pam_google_authenticator)[5654]: debug: start of google_authenticator for "ummeegge"
Mar 11 14:53:00 ipfire google-auth-openvpn(pam_google_authenticator)[5654]: debug: Secret file permissions are 0400. Allowed permissions are 0600
Mar 11 14:53:00 ipfire google-auth-openvpn(pam_google_authenticator)[5654]: debug: "/var/ipfire/ovpn/accounting/google-authenticator/ummeegge" read
Mar 11 14:53:00 ipfire google-auth-openvpn(pam_google_authenticator)[5654]: debug: shared secret in "/var/ipfire/ovpn/accounting/google-authenticator/ummeegge" processed
Mar 11 14:53:00 ipfire google-auth-openvpn(pam_google_authenticator)[5654]: debug: google_authenticator for host "(null)"
Mar 11 14:53:00 ipfire google-auth-openvpn(pam_google_authenticator)[5654]: debug: no scratch code used from "/var/ipfire/ovpn/accounting/google-authenticator/ummeegge"
Mar 11 14:53:00 ipfire google-auth-openvpn(pam_google_authenticator)[5654]: Accepted google_authenticator for ummeegge
Mar 11 14:53:00 ipfire google-auth-openvpn(pam_google_authenticator)[5654]: debug: "/var/ipfire/ovpn/accounting/google-authenticator/ummeegge" written
Mar 11 14:53:00 ipfire google-auth-openvpn(pam_google_authenticator)[5654]: debug: end of google_authenticator for "ummeegge". Result: Success
Mar 11 14:53:00 ipfire openvpnserver[5652]: 192.168.123.4:41732 PLUGIN_CALL: POST /usr/lib/openvpn/plugins/openvpn-plugin-auth-pam.so/PLUGIN_AUTH_USER_PASS_VERIFY status=0
Mar 11 14:53:00 ipfire openvpnserver[5652]: 192.168.123.4:41732 TLS: Username/Password authentication succeeded for username 'ummeegge' 
Mar 11 14:53:00 ipfire openvpnserver[5652]: 192.168.123.4:41732 Outgoing Data Channel: Cipher 'AES-256-GCM' initialized with 256 bit key
Mar 11 14:53:00 ipfire openvpnserver[5652]: 192.168.123.4:41732 Incoming Data Channel: Cipher 'AES-256-GCM' initialized with 256 bit key
Mar 11 14:53:00 ipfire openvpnserver[5652]: 192.168.123.4:41732 Control Channel: TLSv1.3, cipher TLSv1.3 TLS_CHACHA20_POLY1305_SHA256, 2048 bit RSA
Mar 11 14:53:00 ipfire openvpnserver[5652]: 192.168.123.4:41732 [otptest] Peer Connection Initiated with [AF_INET]192.168.123.4:41732
Mar 11 14:53:00 ipfire openvpnserver[5652]: otptest/192.168.123.4:41732 OPTIONS IMPORT: reading client specific options from: /var/ipfire/ovpn/ccd/otptest
Mar 11 14:53:00 ipfire openvpnserver[5652]: otptest/192.168.123.4:41732 MULTI_sva: pool returned IPv4=10.63.16.14, IPv6=(Not enabled)
Mar 11 14:53:00 ipfire openvpnserver[5652]: otptest/192.168.123.4:41732 MULTI: Learn: 10.63.16.14 -> otptest/192.168.123.4:41732
Mar 11 14:53:00 ipfire openvpnserver[5652]: otptest/192.168.123.4:41732 MULTI: primary virtual IP for otptest/192.168.123.4:41732: 10.63.16.14
Mar 11 14:53:01 ipfire openvpnserver[5652]: otptest/192.168.123.4:41732 PUSH: Received control message: 'PUSH_REQUEST'
Mar 11 14:53:01 ipfire openvpnserver[5652]: otptest/192.168.123.4:41732 SENT CONTROL [otptest]: 'PUSH_REPLY,route 10.63.16.1,topology net30,ping 10,ping-restart 60,redirect-gateway,route 192.168.234.0 255.255.255.0,dhcp-option DNS 192.168.123.222,dhcp-option DNS 8.8.4.4,ifconfig 10.63.16.14 10.63.16.13,peer-id 0' (status=1)

. Needed to modify the script a little in this line ‘$MFA_DIR/otp-config/$user_id’ since ‘otp-config’ is not there and are not created via google-authenticator.
Another one is, the barcode comes not up with the script via terminal…

Have extend the script a little but also the package but am stalled currently since there is a discussion if not signed packages should be provided in here in general.

Either way good work !

Best,

Erik

Hey Erik,

I’m happy that it works for you too. Yes the folder /var/ipfire/ovpn/google-authenticator and /var/ipfire/ovpn/google-authenticator/otp-config were created manually because i wanted an output directory for the secret files an the otp-config files. The directories can be created during installation. The barcode is a picture so if you redirect the output into a file the barcode is not there. But with the URL you can show the barcode.

Greetings

Steffen

Hi all,
made now an package update which includes also an extended script which can be found in here --> https://gitlab.com/ummeegge/google-authenticator-openvpn/-/blob/master/build_files/CONF/google-auth-openvpn/google-auth-adduser .
Script includes now:

  • Add new user
  • Display QR-Code or secret for already existing users
  • Delete existing user
  • List all users
  • Modify OpenVPN server and client config

Script can surely be extended and can surely even be made better but for the first this is how it goes.

  • The PAM profile is also included in the package.
  • A new directory under /var/ipfire/ovpn/accounting/google-authenticator will be created which is the home for all new OTP-OpenVPN users.

Packages are now here --> https://people.ipfire.org/~ummeegge/google-authenticator-openvpn/ located.

All build files can be found in here --> https://gitlab.com/ummeegge/google-authenticator-openvpn for those of you who want to build in their environment. Since the uninstall.sh scripts do not use Pakfire but uninstalls the files too, it is currently not on IPFire Git.

Best,

Erik

2 Likes

I find it a very, very interesting functionality. Have you thought about creating an addon and implementing it in the list of installable addons?

It would be a very good feature.

Hi Roberto,
thanks for you positive feedback. Am currently thinking about how it could looks like.
If a OTP functionality comes to IPFire, more then one application like OpenVPN can participate from this (SSH e.g. what else ?) which brings this topic then to a new level (can hear the whispering of a new CGI in the leaves :grimacing: ) but in this current state here i wanted to find a proper way with OpenVPN, may we can also participate of this work here even it would come as a generalized Addon (no script, no PAM config).

I ask myself also:

There are several questions from my side, do someone else have some too ?

Best,

Erik

I’d like to see two factor authorization (2FA) with Authy. Is that possible?

Or FIDO2 ?

Hi all,

can´t find the sources to build it. Am right that you will always need a third party ?

Are there some links for building it from source and an OpenVPN implementation ?

Best,

Erik

EDIT: Interesting ones from my side:
https://www.saltwaterc.eu/setting-up-totp-for-openvpn-with-oath-toolkit.html

I think Authy authenticator and google Authenticator work the same way. Here is info about the google Authenticator:

disclaimer: keep in mind I know nothing about these type of authenticators.

I picked the Authy system because I am not a big fan of google…

This may help:

Hi Jon,
and thanks for the links may i oversee something but i do miss the sources.

Have build oathtool-kit (with Fedora Glib patch) which needed xmlsec1 as DEP --> https://people.ipfire.org/~ummeegge/oathtool/ and made a fast test like explained in here --> https://johannes.truschnigg.info/blog/2015-10-26 .

What was needed:

  • pam_listfile.so was needed. It is build in IPFire but it is commented in ROOTFILE so it is not presant in the main system.
  • Created a secret with oathtool with the following command
    oathtool -v --totp $(openssl rand -hex 15)
    in HEX and Base32 format. Both can be checked via commandline:
Hex secret: ec2eb5fed5a47ae460f492c6ed190d
Base32 secret: 5QXLL7WVUR5OIYHUSLDO2GIN

both should deliver the same PWD which can be checked for HEX via:
oathtool --totp ec2eb5fed5a47ae460f492c6ed190d
for Base32:
oathtool --totp -b 5QXLL7WVUR5OIYHUSLDO2GIN
both should display the same.

  • New directory /var/ipfire/ovpn/accounting/oath .
  • In there are two files, the first is ‘users_oath’ with the follwing content:
#PROTO		USER	-	SECRET				COUNTER	LASTOTP	TS
HOTP/T30	paterpan	-	024d3bd325ad769815ceb59ce4d8bf

and the second users_whitelist with the username(s):

paterpan
  • The PAM config was created under /etc/pam.d/openvpn-otp-oath with the following content:
# Check if the given username is in the list of allowed names
auth requisite pam_listfile.so file=/var/ipfire/ovpn/accounting/oath/users_whitelist item=user sense=allow onerr=fail
# Check for users' time-based One-Time Password from their OATH token device/app
auth requisite /usr/lib/security/pam_oath.so usersfile=/var/ipfire/ovpn/accounting/oath/users_oath window=10 digits=6

# Permit whitelisted usernames - if this is missing, getpwnam() will fail for non-system users
account sufficient pam_listfile.so file=/var/ipfire/ovpn/accounting/oath/users_whitelist item=user sense=allow onerr=fai
  • OpenVPN server.conf has been extended via “Additional config” with the following entry:
# Oathtool + PAM
plugin /usr/lib/openvpn/plugins/openvpn-plugin-auth-pam.so "openvpn-otp-oath"

and client.ovpn like before with:

#Username - Password Authentication
auth-user-pass

#Do not cache auth info
auth-nocache

Connection worked:

Mar 16 14:45:19 ipfire-server openvpnserver[28948]: 192.168.123.4:49345 peer info: IV_TCPNL=1
Mar 16 14:45:19 ipfire-server openvpn[28949]: PAM _pam_init_handlers: no default config other
Mar 16 14:45:19 ipfire-server openvpnserver[28948]: 192.168.123.4:49345 PLUGIN_CALL: POST /usr/lib/openvpn/plugins/openvpn-plugin-auth-pam.so/PLUGIN_AUTH_USER_PASS_VERIFY status=0
Mar 16 14:45:19 ipfire-server openvpnserver[28948]: 192.168.123.4:49345 TLS: Username/Password authentication succeeded for username 'paterpan' 
Mar 16 14:45:19 ipfire-server openvpnserver[28948]: 192.168.123.4:49345 Outgoing Data Channel: Cipher 'AES-256-GCM' initialized with 256 bit key
Mar 16 14:45:19 ipfire-server openvpnserver[28948]: 192.168.123.4:49345 Incoming Data Channel: Cipher 'AES-256-GCM' initialized with 256 bit key
Mar 16 14:45:19 ipfire-server openvpnserver[28948]: 192.168.123.4:49345 Control Channel: TLSv1.3, cipher TLSv1.3 TLS_CHACHA20_POLY1305_SHA256, 2048 bit RSA
Mar 16 14:45:19 ipfire-server openvpnserver[28948]: 192.168.123.4:49345 [oathtest] Peer Connection Initiated with [AF_INET]192.168.123.4:49345
Mar 16 14:45:19 ipfire-server openvpnserver[28948]: oathtest/192.168.123.4:49345 OPTIONS IMPORT: reading client specific options from: /var/ipfire/ovpn/ccd/oathtest
Mar 16 14:45:19 ipfire-server openvpnserver[28948]: oathtest/192.168.123.4:49345 MULTI_sva: pool returned IPv4=10.63.16.18, IPv6=(Not enabled)
Mar 16 14:45:19 ipfire-server openvpnserver[28948]: oathtest/192.168.123.4:49345 MULTI: Learn: 10.63.16.18 -> oathtest/192.168.123.4:49345
Mar 16 14:45:19 ipfire-server openvpnserver[28948]: oathtest/192.168.123.4:49345 MULTI: primary virtual IP for oathtest/192.168.123.4:49345: 10.63.16.18
Mar 16 14:45:20 ipfire-server openvpnserver[28948]: oathtest/192.168.123.4:49345 PUSH: Received control message: 'PUSH_REQUEST'
Mar 16 14:45:20 ipfire-server openvpnserver[28948]: oathtest/192.168.123.4:49345 SENT CONTROL [oathtest]: 'PUSH_REPLY,route 10.63.16.1,topology net30,ping 10,ping-restart 60,redirect-gateway,route 192.168.234.0 255.255.255.0,dhcp-option DNS 192.168.123.1,dhcp-option DNS 192.168.234.1,ifconfig 10.63.16.18 10.63.16.17,peer-id 0' (status=1)

The difference:

  • Another PAM modul was needed.
  • This config includes currently no system users under passwd.
  • Only OTP for authentication needed with this config, no user password.

OTP can be checked via

watch oathtool --totp -b 5QXLL7WVUR5OIYHUSLDO2GIN

whereby every 30sec´s the OTP is changing.

To generate the QR-Code you would need libqrencode and e.g. the following command should it display:

qrencode -t ANSIUTF8 5QXLL7WVUR5OIYHUSLDO2GIN

Some more testings here.

Best,

Erik

3 Likes

Very impressive! I am so glad you understand this. It is miles over my head! :exploding_head: