NPSTN (standing for NoveltyPSTN or NostalgicPSTN, see is a hobbyist VoIP-based phone network for phone collectors and phreaks. While similar to the popular CNET (or Collector’s Network,, NPSTN is considered more phreak-friendly than CNET (dig out your blue box, seriously), but also has a handful of other differences like using conventional 7-digit telephone numbers, and offering both payphone trunks and MF trunks. Both networks are home to people looking to hook up vintage telephone hardware like payphones, teletypes, and even electromechanical switches, but NPSTN seemed more interesting to me and I decided to pursue that first.

The network functions fairly simply, allowing for each operator to run Asterisk phone switch software on a machine of their choosing to create their own Central Office. To avoid number collisions, operators register directly with NPSTN to reserve their own office code and get a “thousands-block” of numbers (such as 123-4XXX, where the operator chooses the first four digits). When an operator (or anyone connected to the operator’s PBX) places a call to a phone number outside of their office code, it is routed over the NPSTN network using the IAX2 protocol to get to the proper office (another machine running Asterisk for the office code dialed) where it is handed to the proper local extension.

While this is an imperfect metaphor, the NPSTN network mirrors a lot of the functionality behind how the traditional telephone network used to operate, albeit with modern hardware and software. Note that NPSTN does not let you dial out to the traditional PSTN by default; you won’t be able to call your friend’s cell phone or anything like that unless you configure your own egress. Think of NPSTN like a darknet for telephony where people connect interesting hardware to experiment with and allow others on the network to use.

If you want to read more about NPSTN, they have thorough documentation available here, Below, I’ve created something of a quick-start guide for joining the network and getting running with minimal effort.

Joining NPSTN

The first step you will need to take to join the network is to register with NPSTN and reserve an office code. Simply fill out the form here,, which should be fairly straight-forward. Two fields that may not seem completely obvious are Protocol and IAX2/SIP Username, where you should be safe to use IAX2 and npstn respectively. Something that might throw you through a loop is the IP Address or FQDN (fully qualified domain name) field for your machine running Asterisk. While many people on NPSTN choose to host their exchange on a VPS or dedicated server in a datacenter somewhere with a static IP address, I host mine on a Raspberry Pi at home. Due to this, my machine will be accessible via a dynamic IP address that could change periodically so I have a script to update a FQDN whenever my IP address changes and I use this FQDN with NPSTN. If you don’t own a domain, you can accomplish the same task with a dynamic DNS service like FreeDNS,

Installing and Configuring Asterisk

After submitting your request, you will receive a UCP (User Control Panel) Key, so until that happens we can get a machine running Asterisk set up. As I mentioned earlier, I run my exchange off of an older Raspberry Pi–you really don’t need a powerful or new machine to run Asterisk. For the below steps, I am going to assume you are running a Debian-based system.

First, let’s get a root shell:

$ sudo -i

Now let’s install ntp and set out timezone. Note that this is the timezone of where the machine is, not necessarily the timezone of where you are.

# apt install -y ntp
# timedatectl set-timezone America/New_York

Next, we will download and install asterisk. NPSTN recommends version 13, which is still supported by Asterisk.

# cd /tmp
# wget
# tar -xzf asterisk-13-current.tar.gz
# cd asterisk-13.*/
# ./configure
# make && make install
# make samples

Now that Asterisk is installed and set up with some basic configuration files, we will go ahead and replace necessary configuration and audio files using NPSTN’s boilerplate samples:

# cd /etc/asterisk/
# rm iax.conf
# rm sip.conf
# rm musiconhold.conf
# rm extensions.conf
# wget
# wget
# wget
# wget
# wget
# cd /var/lib/asterisk/sounds/en/
# mkdir custom
# cd custom
# mkdir signal
# cd signal
# wget
# wget
# wget
# wget
# wget
# cd /var/lib/asterisk/moh/
# mkdir ringback
# cd ringback
# wget

Pretty easy so far!

Configuring the Dialplan

Now we will edit extensions.conf with info specific for the your office. You’re going to need a unique CLLI for your switch, which generally acts as a name. To be compliant with NPSTN standards, these names begin with NPSTN and are followed by 4-8 more characters of location information or function. Because my switch is the first in the Southeastern Pennsylvania region, I used NPSTNSEPA01 as my CLLI. After you settle on a CLLI, you will also need to have the UCP Key sent to you after registering before proceeding. Below is my extensions.conf configuration for the 892-7 thousands-block. You should be able to compare this to the boilerplate configuration and read the comments to figure out hot to apply your own switch details. You’ll likely only need to make small modifications to the first 50 lines or so!

;;;;;;;;; NPSTN NA 201905011 - Boilerplate Template for New NPSTN Nodes
#include verification.conf ; this includes /etc/asterisk/verification.conf in the main Asterisk dialplan (loaded via extensions.conf)

[globals] ; Global variables that persist across all calls are defined here
; "Core" variables that MUST be defined on ALL NPSTN nodes
clli=NPSTNSEPA01 ; the unique CLLI of your node
zipcode=19086 ; your ZIP code (00000 for nodes outside the U.S.A.)
maindisa=8927111 ; number to your primary DISA (if you don't have one, then any number that comes into your switch from NPSTN)
allowdisathru=YES ; See to learn more about what this variable does
allowpstnthru=YES ; See to learn more about what this variable does
npstnkey=REDACTED ; your NPSTN UCP auth key (dial the business office at 116 or (407) 564-4141 if you need one)

; NOTE: NPSTN has a "billing" functionality that is purely for fun. The idea is to show you all the calls you make on NPSTN and what they would have cost back in the day. The bill is for fun, and you are not actually charged anything. NPSTN is and always will be 100% free.

; "Add-on" variables that are used only for convenience purposes on this node - in this case to define the path to audio files
dialtone=custom/signal/dialtone ; File path to audio file
busy=custom/signal/busy ; File path to audio file
reorder=custom/signal/reorder ; File path to audio file
ccad=custom/signal/ccad ; File path to audio file
acbn=custom/signal/acbn ; File path to audio file

; Number range assignments assignments
OC1=892 ; your office code's NNX (NOT NNX-X, whether or not you have all its thousands blocks) - create OC2, OC3, etc. if you have multiple blocks
                ; This sample implementation assumes the thousand blocks 555-1 and 555-9 have been assigned (adjust accordingly based on YOUR assignment)
                ; The individual extensions pre-created in [local] all adhere to the NPSTN numbering standards

[local] ; Define all local extensions here, except ATA lines which are automatically covered by the [ata] fallthrough
;exten => ${OC1}7XXX,1,GoSub(SIPxNumToPeer,s,1(${EXTEN}))
;       same => n,GoToIf($["${GOSUB_RETVAL}"="000"]?intercept:ata)
;       same => n(ata),GoSub(ata,s,1(${GOSUB_RETVAL})) ; Dial SIP peer if it exists
;       same => n,Hangup()
;       same => n(intercept),GoSub(ccad,s,1) ; Intercept message if SIP peer does not exist
;       same => n,Hangup()
exten => _${OC1}7XXX,1,GoSub(ata,s,1(${EXTEN})) ; See if any other numbers in this number block are possibly ATAs
        same => n,Hangup()
; exten => _${OC1}9XXX,1,GoTo(ata,${EXTEN},1) ; See if any other numbers in this number block are possibly ATAs
exten => ${OC1}7111,1,GoTo(dt,s,1) ; DISA access
exten => ${OC1}7701,1,SayAlpha(${clli}) ; "Switch verification"
        same => n,Hangup()
exten => ${OC1}7731,1,GoTo(echotest,s,1) ; Echo test
exten => ${OC1}7732,1,Answer() ; Calls that may last a while that won't get answered by a real person are best answered right away
        same => n,Wait(7200) ; Silent termination
        same => n,Hangup()
exten => ${OC1}7758,1,SayDigits(${CALLERID(num)}) ; a primitive ANAC
        same => n,Hangup()
exten => ${OC1}7760,1,GoSub(mwtone,s,1) ; Milliwatt test tone
        same => n,Hangup()
exten => ${OC1}7970,1,GoSub(busy,s,1) ; Always busy
        same => n,Hangup()
exten => ${OC1}7771,1,GoSub(reorder,s,1) ; Always reorder
        same => n,Hangup()
exten => ${OC1}7790,1,Answer() ; Calls that may last a while that won't get answered by a real person are best answered right away
        same => n,Dial(Local/ring@ringout,,m(ringback)) ; Number that always rings "forever"
;exten = _X.,1,Verbose(1, "User ${CALLERID(num)} dialed an invalid number.")
;       same =  n,Playback(pbx-invalid)
;       same = n,Hangup()

exten => ring,1,Wait(7200)
        same => n,Hangup()

[private] ; Any extensions you want to be able to dial but want to keep off-limits to outside callers
exten => 9876,1,Answer()
        same => n,Hangup()

;;; Any additional contexts you need
[echotest] ; Echo test
exten => s,1,Answer()
        same => n,Echo()
        same => n,Hangup()

[mwtone] ; Milliwatt test tone
exten => s,1,SET(VOLUME(TX)=8)
        same => n,Wait(0.5)
        same => n,PlayTones(1004/1000)
        same => n,Wait(600)
        same => n,SET(VOLUME(TX)=1)
        same => n,Return

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; You should not need to touch anything below here ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[from-npstn] ; Incoming calls from NPSTN
exten => _X!,1,GoSub(npstn-verify,${EXTEN},1)
        same => n,GotoIf($["${clidverif}"="11"]?:accept)
        same => n,GotoIf($[${GROUP_COUNT(npstnspam@cvs)}<5]?:reject)
        same => n(accept),GoTo(external-users,${EXTEN},1)
        same => n(reject),Hangup()

[external-users] ; External calls from various places (NPSTN, possibly C*NET, PSTN, and other networks, arrive here)
include => local ; Include the contents of the [local] context

include => private ; Note: accessible only by *direct* dialing from ATA lines (not via DISA)
include => local
include => longdistance ; If the number dialed is not local, go "out" to NPSTN

[from-internal] ; Incoming calls from local SIP devices
exten => 8,1,GoTo(dt,s,1) ; Use extension "8" for ATA off-hook auto-dial for immediate NPSTN city dialtone (could likewise do 9 for PSTN, 7 for C*NET, etc.)
exten => _X!,1,Set(CHANNEL(hangup_handler_push)=npstnbilling,${EXTEN},1(${STRFTIME(${EPOCH},,%s)},${STRFTIME(${EPOCH},,%Y-%m-%d %H:%M:%S)}))
        same => n,GoTo(internal-users,${EXTEN},1)

[SIPxNumToPeer] ; NPSTNNA 20191130 ; Returns SIP peer of a number or 000 if peer/number does not exist
exten => s,1,Set(num=${ARG1})
        same => n,GotoIf($["${num}"=""]?dne)
        same => n,Set(scriptexists=${STAT(e,/etc/asterisk/scripts/})
        same => n,GotoIf($["${scriptexists}"="1"]?use)
        same => n(download),SYSTEM(wget -P /etc/asterisk/scripts/)
        same => n(perm),SYSTEM(sudo chmod 777 /etc/asterisk/scripts/
        same => n(use),Set(peer=${SHELL(/etc/asterisk/scripts/ "${num}")})
        same => n,GotoIf($["${peer}"=""]?dne)
        same => n,Return(${peer})
        same => n(dne),Return(000)

[ata] ; Automatically dials the SIP peer by name (ARG1 = SIP peer name)
exten => s,1,Dial(SIP/${ARG1},,m(ringback)g)
        same => n,GotoIf($["${DIALSTATUS}"="CHANUNAVAIL"]?reorder,1)
        same => n,GotoIf($["${DIALSTATUS}"="BUSY"]?busy,1)
        same => n,GotoIf($["${DIALSTATUS}"="CONGESTION"]?reorder,1)
        same => n,GotoIf($["${DIALSTATUS}"="NOANSWER"]?reorder,1)
        same => n,Return()
exten => busy,1,GoSub(busy,s,1)
        same => n,Return()
exten => reorder,1,GoSub(reorder,s,1)
        same => n,Return()
exten => ccad,1,GoSub(ccad,s,1)
        same => n,Return()
exten => acbn,1,GoSub(allcktsbusy,s,1)
        same => n,Return()

[busy] ; Busy signal (~60ipm)
exten => s,1,Playback(${busy},noanswer)
        ;same => n,GoTo(1) ; to loop the busy signal, uncomment this line
        same => n,Return()

[reorder] ; Reorder signal (~120ipm)
exten => s,1,Playback(${reorder},noanswer)
        ;same => n,GoTo(1) ; to loop the reorder signal, uncomment this line
        same => n,Return()

[ccad] ; Cannot Be Completed As Dialed Intercept
exten => s,1,Playback(${ccad},noanswer)
        same => n,Return()

[allcktsbusy] ; All Circuits Busy Intercept
exten => s,1,Playback(${acbn},noanswer)
        same => n,Return()

[dt] ; NPSTN dialtone
exten => s,1,Answer()
        same => n(begin),Set(num=)
        same => n,Read(num1,${dialtone},1)
        same => n,Set(num=${num}${num1})
        same => n,GotoIf($["${num}"=""]?permsig)
        same => n,GotoIf($["${num}"="0"]?done)
        same => n,Read(num2,,1)
        same => n,Set(num=${num}${num2})
        same => n,Read(num3,,1)
        same => n,Set(num=${num}${num3})
        same => n,GotoIf($["${num1}${num2}"="11"]?done)
        same => n,GotoIf($["${num2}${num3}"="11"]?done)
        same => n,GotoIf($["${num1}"="1"]?ld8)
        same => n,GotoIf($["${num}"="660"]?done)
        same => n,GotoIf($["${num}"="958"]?done)
        same => n,GotoIf($["${num}"="959"]?done)
        same => n,Read(station,,4)
        same => n,Set(num=${num}${station})
        same => n,GoTo(done)
        same => n(ld8),Read(station,,5)
        same => n,Set(num=${num}${station})
        same => n(done),Set(__ticketed=0)
        same => n,Set(CHANNEL(hangup_handler_push)=npstnbilling,${num},1(${STRFTIME(${EPOCH},,%s)},${STRFTIME(${EPOCH},,%Y-%m-%d %H:%M:%S)}))
        same => n,GoTo(dtdest,${num},1)
        same => n(permsig),GoTo(dtdest,permsig,1)
exten => h,1,GotoIf($["${ticketed}"="0"]?:done)
        same => n,GoSub(npstnbilling,${dtnum},1(${ss},${tt}))
        same => n(done),Hangup()

[dtdest] ; Routes calls from [dt]
exten => permsig,1,GoSub(dialnpstn,start,1(,disable,${zipcode},${maindisa}))
        same => n,Hangup()
include => local
include => dtnpstn

[longdistance] ; Outgoing NPSTN access for internal users
exten => _X!,1,GoTo(dtnpstn,${EXTEN},1)

exten => _X!,1,Set(originalclid=${CALLERID(all)}) ; Any other calls should be sent "out" to NPSTN
        same => n,GoSub(npstn-out-verify,${EXTEN},1)
        same => n,GotoIf($["${GOSUB_RETVAL}"="0"]?npstn-out-blocked,s,1)
        same => n,GoSub(dialnpstn,start,1(${EXTEN},disable,${zipcode},${maindisa}))
        same => n,Set(CALLERID(all)=${originalclid})
        same => n,Hangup()

[dialnpstn] ; NPSTN 20190215 NA ; Sends a call to NPSTN
exten => start,1,Set(num=${ARG1})
        same => n,GotoIf($[${LEN("${ARG4}")}>2]?clidoverride)
        same => n,Set(lookup=${SHELL(curl "${ARG1}&cid=${CALLERID(num)}&sntandem=${ARG2}&zipcode=${ARG3}")})
        same => n,GoTo(verify)
        same => n(clidoverride),Set(lookup=${SHELL(curl "${ARG1}&cid=${ARG4}&sntandem=${ARG2}&zipcode=${ARG3}")})
        same => n(verify),GoSub(lookupchan,s,1(${lookup})) ; verifies lookup for extra security
        same => n,GotoIf($["${GOSUB_RETVAL}"="0"]?npstn-BLOCKED,1)
        same => n,NoOp(NPSTN ROUTE TO: ${lookup})
        same => n,Dial(${lookup},,g)
        same => n,GoTo(npstn-${DIALSTATUS},1)
exten => npstn-ANSWER,1,Return()
exten => npstn-BUSY,1,GoSub(busy,s,1)
        same => n,Return()
exten => npstn-CONGESTION,1,GoSub(allcktsbusy,s,1)
        same => n,Return()
exten => npstn-CHANUNAVAIL,1,GoSub(reorder,s,1)
        same => n,Return()
exten => npstn-NOANSWER,1,GoSub(allcktsbusy,s,1)
        same => n,Return()
exten => npstn-BLOCKED,1,GoSub(ccad,s,1)
        same => n,Return()
exten => _npstn-.,1,Return()

[npstnbilling] ; NPSTN NA 20190504 NPSTN Toll Ticketing; EXTEN= number dialed, ARG1= start time of call in seconds, ARG2= datetime that the call started
exten => _X!,1,GoToIf($["${ARG1}"=""]?done)
        same => n,Set(endtime=${STRFTIME(${EPOCH},,%s)})
        same => n,Set(duration=$[${endtime} -${ARG1}]) ; can't use ${CDR(billsec)}
        same => n,GoToIf($["${ARG3}"=""]?:special)
        same => n,Set(type=direct)
        same => n,GoTo(request)
        same => n(special),Set(type=${ARG3})
        same => n(request),Set(request=${SHELL(curl "${FILTER(0-9A-Za-z,${CALLERID(num)})}&callee=${FILTER(0-9,${EXTEN})}&type=${type}&duration=${duration}&key=${npstnkey}&clli=${clli}&year=${ARG2:0:4}&month=${ARG2:5:2}&day=${ARG2:8:2}&hr=${ARG2:11:2}&min=${ARG2:14:2}&sec=${ARG2:17:2}")})
        same => n(done),Set(__ticketed=1)
        same => n,Return()

To make sure that your system can be reached by NPSTN, make sure that you forward (if you sit behind a gateway) or open (if you have a firewall set up) UDP port 4569.

Configuring SIP Peers

Now we also want to set up a local phone or two to interact with our switch and NPSTN as a whole. I connect vintage physical phones to my Asterisk machine though SIP-enabled ATA (analog telephone adapter) devices that essentially act as intermediaries between an old phone and the Asterisk switch. The most simple ATA will have both an RJ-11 port to connect the phone, and an RJ-45 port to plug the device into your LAN. When connected, the ATA can be configured (usually through a web interface) with credentials for Asterisk so the phone can take and place calls.

As an alternative option, there are SIP clients for many operating systems that will serve the same purpose and allow you to make calls through and into your switch through your computer or mobile device.

Before you connect a SIP client or an ATA to the switch, you need to modify the sip.conf configuration file to expect connections from your device(s). Here is an example of my sip.conf with two peers: 8927181 and 8927189:

bindport=16555   ;set it to a UDP port and never tell anyone what it is! Do not forward the port in your router
;do not use port 5600 for external SIP connections; change your port to something non-standard i.e. bindport=39145 pick a port between 16383 and 65535 and never tell anyone what port you are using! Do not forward that UDP port in your router and ensure that UDP port 5600 is not forwarded in your router either. Indeed you should not need RTP UDP ports (usually 10000-20000) forwarded either.
allowguest=no ;keep intruders out
alwaysauthreject=yes    ;make life difficult for scanners trying to find a way into your dialplan
nat=force_rport,comedia ;should make nat more secure
tos_sip=cs3      ; Sets TOS for SIP packets.
tos_audio=ef     ; Sets TOS for RTP audio packets.
threewaycalling = yes
transfer = yes

[ATAs](!) ; template for all ATA logins
type = peer
host = dynamic
qualify = yes
insecure = port,invite
canreinvite = no ; don't allow RTP voice traffic to bypass Asterisk
relaxdtmf = yes
progressinband = yes

[8927181](ATAs) ; SIP login for user 8927181
defaultuser = 8927181
secret = yourpasswordhere
authid = 8927181
callerid = "Mide Dank" <8927181> ; change the CNAM and caller ID of your line here
context = from-internal ; context in the dialplan in which this user originates a call

[8927189](ATAs) ; SIP login for user 8927189
defaultuser = 8927189
secret = yourpasswordhere
authid = 8927189
callerid = "Mide Dank" <8927189> ; change the CNAM and caller ID of your line here
context = from-internal ; context in the dialplan in which this user originates a call

For the 8927181 number, I am using a Gradstream HT704 device with mostly default settings. Below, I am configuring Profile 1 on the device to connect to my PBX at As you can see, I set the fields for Primary SIP Server and Outbound Proxy to use this address. I think the only other thing I did on this page was change Enable Pulse Dialing to Yes. When finished, click on the Apply button at the bottom of the page.

The Profile 1 configuration on the Grandstream.

The Profile 1 configuration on the Grandstream.

Next, I went to the FXS Ports tab to enter the SIP credentials. This is fairly basic, and you just need to make sure that the credentials match what they are in Asterisk’s sip.conf while also selecting Profile 1 to use what we just set up (and don’t forget to check the option for Enable Port). Again, Apply the settings, but then also Reboot so they can take effect.

Configuring the FXS Ports on the Grandstream.

Configuring the FXS Ports on the Grandstream.

For the 8927189 number, I set this up with the Android application Zoiper. All I needed to do here was add an account to the application with the corresponding SIP credentials from sip.conf and server address ( again), and everything was set up in just as few clicks.

Zoiper configuration.

Zoiper configuration.

Start on System Boot

Now let’s create a systemd unit file so that Asterisk will start up when the system does. First we create a unit file:

# nano /etc/systemd/system/asterisk.service

Paste the following and save with ctrl-x:

Description=Asterisk PBX and telephony daemon

ExecStart=/usr/sbin/asterisk -f -C /etc/asterisk/asterisk.conf
ExecStop=/usr/sbin/asterisk -rx 'core stop now'
ExecReload=/usr/bin/asterisk -rx 'core reload'


Now we can enable and start asterisk:

# systemctl daemon-reload
# systemctl enable asterisk
# systemctl start asterisk

That’s it! If all goes well your PBX should spring to life.

Explore Your Switch and NPSTN

Now that everything is configured, you can start testing your switch. The first test you’ll probably want to do is to dial into your DISA (Direct Inward System Access) which gives you access directly into your switch. This is typically your NNN-N111 extension (so mine would be 892-7111). If all goes well you should hear a different dial tone. You can also dial several different test numbers on your switch by consulting extensions.conf to see what is set up. Here’s the relevant excerpt from my configuration above that you already (hopefully) modified for your own block:

exten => ${OC1}7111,1,GoTo(dt,s,1) ; DISA access
exten => ${OC1}7701,1,SayAlpha(${clli}) ; "Switch verification"
        same => n,Hangup()
exten => ${OC1}7731,1,GoTo(echotest,s,1) ; Echo test
exten => ${OC1}7732,1,Answer() ; Calls that may last a while that won't get answered by a real person are best answered right away
        same => n,Wait(7200) ; Silent termination
        same => n,Hangup()
exten => ${OC1}7758,1,SayDigits(${CALLERID(num)}) ; a primitive ANAC
        same => n,Hangup()
exten => ${OC1}7760,1,GoSub(mwtone,s,1) ; Milliwatt test tone
        same => n,Hangup()
exten => ${OC1}7970,1,GoSub(busy,s,1) ; Always busy
        same => n,Hangup()
exten => ${OC1}7771,1,GoSub(reorder,s,1) ; Always reorder
        same => n,Hangup()
exten => ${OC1}7790,1,Answer() ; Calls that may last a while that won't get answered by a real person are best answered right away
        same => n,Dial(Local/ring@ringout,,m(ringback)) ; Number that always rings "forever"

To find numbers on NPSTN that you can access, take a look at the directory and see if there is anything that interests you, There are literally thousands of operational numbers on the network!


Hopefully by this point you have a working NPSTN node that you can grow with extensions of your own. As you add more extensions to the network, make sure you use your UCP Key to log into the control panel ( and update the directory!

In the future, I’ll go into detail about hooking your PBX into your residential phone line so you can make outgoing calls and have incoming calls come into your PBX directly.

In the meantime, check out some of my older articles about setting up a PBX with FreePBX,

Special thanks to Naveen Albert who was essential in helping me get my system running properly, and the members of the PhreakNet chat!