Skip to content

Instantly share code, notes, and snippets.

@lgaetz
Last active December 21, 2022 10:18
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lgaetz/066c655867b4e679da21dbd1567b29bd to your computer and use it in GitHub Desktop.
Save lgaetz/066c655867b4e679da21dbd1567b29bd to your computer and use it in GitHub Desktop.
; Latest version: https://gist.github.com/lgaetz/066c655867b4e679da21dbd1567b29bd
;
; Crude proof of concept of 2FA for a FreePBX extension
; Clients can register to the PBX, but are unable to dial until they successfully
; enter a random PIN sent via email.
; Once the PIN passes, the client IP is whitelisted and user can dial without
; restriction until the IP changes
;
; License: GNU GPL3+
;
; usage: put this code in extensions_custom.conf and change context of extension to from-mac
;
; History
; 2021-10-14 First commit, NOT SUITABLE FOR PRODUCTION
; 2021-10-18 update notes, add emerg bypass, NOT SUITABLE FOR PRODUCTION
[from-mac]
; only local extensions should be in this context, so dialplan ASSUMES that no external callers will end up here.
; There is no check to confirm the call is from a local extension
exten => _.,1,Noop(Entering user defined context from-mac in extensions_custom.conf)
; Add bypass to allow all emergency call strings - required for some jurisdictions
exten => _.,n,GotoIf(($["${EXTEN}"="911"]?pass)
exten => _.,n,GotoIf(($["${EXTEN}"="933"]?pass)
exten => _.,n,GotoIf(($["${EXTEN}"="999"]?pass)
exten => _.,n,Set(mac_address=${PJSIP_HEADER(read,User-Agent):-12}}))
exten => _.,n,Set(ext_ip=${CUT(CHANNEL(pjsip,remote_addr),:,1)}) ; ip addess of dialing extension
; add check to confim string is an IP address
exten => _.,n,Macro(user-callerid) ; needed to set ampuser var
; check to see if the ip is recognized for this extension
exten => _.,n,gotoif($["${DB(ipwhite/${AMPUSER})}"="${ext_ip}"]?pass)
; calls here need to be challenged
exten => _.,n,set(goal=${RAND(0,9)}${RAND(0,9)}${RAND(0,9)}${RAND(0,9)}) ; generates a 4 digit PIN
; todo - can we hide PIN from being logged?
exten => _.,n,set(email=${VM_INFO(${AMPUSER}@default,email)}) ; get email for vm
; to do, AGI to get email address from userman instead of voicemail
exten => _.,n,TrySystem(echo "PIN ${goal}" | mail -s "2FA validation PIN" ${email}) ; email PIN to caller's vm
exten => _.,n,Read(dtmf_in,enter-password,4,,,60) ; caller gets 60 sec to read email and enter DTMF pin
exten => _.,n,gotoif($["${dtmf_in}"="${goal}"]?whitelist) ; if pin matches, add IP to astdb for whitelisting
exten => _.,n,Noop(challenge failed *************************)
; TODO after multiple failures, add IP to blacklist? send alert?
exten => _.,n,Hangup()
exten => _.,n(whitelist),Noop(Whitelist ip)
exten => _.,n,Set(DB(ipwhite/${AMPUSER})=${ext_ip}) ; add IP to astdb so they don't get challenged again
exten => _.,n(pass),Noop(passed *************************)
exten => _.,n,goto(from-internal,${EXTEN},1)
; Different apporach to the same concept
; Extensions stay in from-internal context, which will allow them to dial local feature codes/extensions, but
; outbound calls are blocked until the calling extension's SIP User agent is known to the system
; Unknown UA will be challenged with PIN
;
; License: GNU GPL3+
;
; usage: put this code in extensions_custom.conf and change context of extension to from-mac
;
; History
; 2021-10-15 First commit, NOT SUITABLE FOR PRODUCTION
; 2021-10-18 Update notes, add emerg bypass, NOT SUITABLE FOR PRODUCTION
[check-user-agent]
exten => s,1,Noop(Entering user defined context check-user-agent in extensions_custom.conf)
; TODO - add check and bypass to ensure this is a call from a local registered extension and NOT an external call being forwarded to an outbound
; route. Prob best done by confirming channel var AMPUSER matches a local extension number
exten => s,n,Set(user_agent=${PJSIP_HEADER(read,User-Agent)}})) ; UA of dialing ext
exten => s,n,Set(mac_address=${PJSIP_HEADER(read,User-Agent):-12}})) ; 12 rightmost chars from UA which could be a MAC
exten => s,n,Set(ext_ip=${CUT(CHANNEL(pjsip,remote_addr),:,1)}) ; ip addess of dialing extension
; check to see if the UA has been previously allowed for this extension
exten => s,n,gotoif($["${DB(ipwhite/${AMPUSER})}"="${user_agent}"]?pass)
; calls here need to be challenged
exten => s,n,set(goal=${RAND(0,9)}${RAND(0,9)}${RAND(0,9)}${RAND(0,9)}) ; generate random 4 digit PIN
; todo - can we hide PIN from being logged?
exten => s,n,set(email=${VM_INFO(${AMPUSER}@default,email)}) ; get email for vm
; to do, AGI to get email address from userman instead of voicemail
; to do, validate that we got a proper email address
exten => s,n,TrySystem(echo "PIN ${goal}" | mail -s "2FA validation PIN" ${email}) ; email PIN to caller's vm
exten => s,n,Read(dtmf_in,enter-password,4,,,120) ; caller gets 120 sec to read email and enter DTMF pin
exten => s,n,gotoif($["${dtmf_in}"="${goal}"]?whitelist) ; if pin matches, add UA to astdb for whitelisting
exten => s,n,Noop(challenge failed *************************)
exten => s,n,Hangup()
exten => s,n(whitelist),Noop(Code entered correctly)
exten => s,n,Playback(auth-thankyou)
exten => s,n,Set(DB(ipwhite/${AMPUSER})=${user_agent}) ; add IP to astdb so they don't get challenged again
exten => s,n(pass),Noop(passed *************************)
exten => s,n,Return()
[macro-dialout-trunk-predial-hook]
exten => s,1,Noop(Entering user defined context macro-dialout-trunk-predial-hook in extensions_custom.conf)
; add bypass for emergency calls. Calls thru outbound routes with EMERGENCY enabled will not be challenged
exten => s,n,GosubIf($["${EMERGENCYROUTE}"!="YES"]?check-user-agent,s,1)
exten => s,n,MacroExit
@lgaetz
Copy link
Author

lgaetz commented Oct 15, 2021

Problems:

  • won't work in this format with multiple contacts per endpoint.
  • whitelisting by IP is not practical for roaming clients, and roaming clients are the most in need. Try UA string from header
  • relies on custom extension context which is less than ideal. Need to hook (or splice) from-internal
  • Need method to auto expire records after fixed period of time
  • If blocking outbound calls, need mechanism to ensure non-local calls using an outbound route are not affected.

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