This tutorial will walk through building an elaborate CatFacts prank in Golang, deployed via Kubernetes. Why CatFacts? Because it’s more interesting than implementing another TODO app, but our end goal is really to design and build a clean service that is easy to maintain going forward.
You can get the finished code (as well as read the deployment guide) for this service here.
Prank at a glance
As a service administrator, you text the phone number of your prank target to the service. The service then launches an attack on your target — and allows you to monitor and stop it at will.
This service prioritizes expediency and precision of command, because successful offensives are founded upon the core principles of speed, violence, and momentum.
Your target is thusly entered into CatFacts hell. They’ll receive text messages at the interval you’ve configured (defaults to 30 seconds) and if they text the number back they’ll get one of several infuriating messages about their command not being recognized or receiving additional CatFacts for free.
If they call the number, they get an additionally hellish phone tree experience, complete with piercing angry meowing sounds. Watch a recording of what the phone tree is like here.
Tech at a glance
How will we implement this prank in code? We know we’re going to use Twilio to handle sending and receiving SMS and phone calls. Twilio’s API allows us to trigger outbound messages and calls, but it also allows us to define the experience a user should have when making an inbound call, such as the calls they’ll make in order to try to figure out how they were subscribed to CatFacts.
In order to do this, Twilio leverages webhooks and makes an HTTP request to an endpoint you describe and implement. It will expect to find TwiML (Twilio Markup Language) at that endpoint in XML format, so our service will need to both host these endpoints and correctly render TwiML to create the phone tree experience.
This takes care of sending and receiving, but we also need a means of starting, managing and stopping “attacks” on our friends and family. Meanwhile, we wouldn’t want just anybody to be able to use our service, because it will have a direct line to our Twilio account which costs money, and because if we go too wide with this prank we’ll get shut down and receive unhappy emails from Twilio. Therefore we’ll need our service to be secure: nobody but Twilio and our blessed administrators who we identify should be able to interact with it.
An overly elaborate CatFacts pranking service is only as useful as its uptime is reliable, because life is uncertain and you never know when someone in your immediate vicinity will become deserving of a torrential dystopian downpour of CatFacts. To ensure our service is robust and self-healing, we’re implementing it as a Dockerized Golang web service running in Kubernetes.
When our service is finished, we want anyone to be able to clone it, quickly add their own Twilio account credentials, add some optional server information like the domain name they’re mapping to it, and the desired port to listen on, and run it without issue.
This means that every such option should be parameterized within our application, and our config format should be simple and clear so that our users can get their own instance working with a minimum of fuss.
This will also follow the configuration tenant, at least, of the 12 factor app pattern: keeping configuration totally separate from code. The litmus test described by the 12 factor app pattern, which we achieve here, is the ability to open-source the application at any point without compromising any credentials or other secrets. In the case of this application, the required config.yml file is gitignored so that it stays out of source control.
With all this in mind, I chose to use the Cobra Golang command library, which is excellent and easy to use. It helps you build executable programs with self-documenting command flags without making a mess. For what it’s worth, Kubernetes uses this library as well. The perfect complement from the same author is Viper, for the actual configuration parsing.
Here we declare the root command and its descriptions, as well as the function, persistentPreRun, that should be executed when our service’s root command is invoked. This function is where we’ll do our configuration parsing and setup.
We also wire up the initConfig function to handle reading a config file that is stored in the same working directory as the application. If we are unable to successfully read or parse the config file on startup, we can bail out with an error.
Likewise, if the user fails to define any required arguments such as their Twilio API key or phone number, we’ll also bail out as we’d be unable to successfully run the prank until they are provided.
Speaking of terminating execution with a helpful error, the logrus structured logging library allows us to do both with a single method, log.Fatal:
Logrus is a great library because it forces you to think in terms of leveled logging and helps you avoid messy and error prone fmt.Sprintf statements everywhere by declaring your fields when logging a message:
We’ve done the work to ensure that our user has supplied all required arguments to our service. Now we can start an HTTP server by listening on the configured port. There are a few other tasks we want to accomplish at this time as well: creating an AttackManager object that abstracts away starting, stopping and checking on attacks, and registering all of our HTTP routes:
Abstracting away all of the operations for managing attacks on targets is one way we can separate concerns and let our HTTP handlers really just be HTTP handlers.
Our manager itself will need a means of keeping track of running attacks, starting new ones, cancelling running ones once we’ve decided our target has had enough, and reporting on the overall status of the system so we can monitor progress and remember who all we’re pranking at any given time (especially important once we add more than one administrator to the mix).
Since we’re writing Go, our AttackManager can be a simple struct which includes a “repository” which is just a slice of Attacks.
An attack is also simple, it’s a struct containing everything we need to know in order to prank someone and report on the prank status later:
In order to extend our AttackManager with additional functionality, we implement new functions with our AttackManager struct indicated as the “receiver” of these methods:
New attack requests get validated (and their target numbers get normalized for clean lookups later). If everything checks out, they get added to our AttackManager’s repository slice of running attacks.
Our attack loop logic is very simple — all we need do is loop through our repository of attacks at a regular interval (which our users can modify via configuration). The default is 30 seconds, so every 30 seconds we’d be looping through the slice of attacks, sending a new message to each target, and incrementing the counter of total attacks sent to each attack instance:
Our CatFacts are defined as a JSON array that gets read by our AttackManager during its initialization:
This allows us to send the messages in sequence to each target — the count of messages sent already is used as input to the function that retrieves the next CatFact message body: first we step them through all 70+ messages in sequence, but once we’ve exhausted all our facts we just send them one at random each time going forward:
Command and control via SMS
One of the more interesting things about this application is that it exposes a secure command and control interface to configured administrators via SMS: an admin need only send the service’s Twilio number a text message containing a prank target to initiate an attack against that target. Likewise, admins may stop attacks in the same way and request a system status message that displays all currently running attacks.
This extremely lightweight interface is key to the prank’s success and usability. You can be sitting next to your friend ( or someone who just screwed you in a minor business transaction ) in a park or a movie theater and launch an attack on them in a few seconds and without raising much suspicion.
This interface further demonstrates our Twilio integration: when you send an SMS to the configured Twilio number, Twilio looks into the configuration we’ve setup via the Twilio dashboard to find the webhook endpoint it should contact.
This endpoint, also exposed securely by our application, is what receives data from Twilio in a form POST body (such as the number who sent the message and the message contents) that it can further process to command the running system as described by the message:
Securing the service
We built the concept of admins into our configuration and attack management — your message won’t be considered by our command processing codepath unless you’re a known administrator.
Anyone else sending an SMS message to our defined Twilio number is assumed to be a pranking target, so they’ll just get another unhelpful CatFacts message about their account status (and free upgrade to a higher tier of service):
This takes care of command and control — but how do we ensure that not just anyone can scrape our service or even call its endpoints? Via a multi layered approach.
First, we ensure our service is only available over HTTPS, which defends against Man in the middle attacks trying to inspect payloads sent between Twilio and our service.
Second, we implement HTTP basic auth, such that you cannot call the endpoints successfully unless you supply the correct username and password secrets when doing so.
Our service automatically checks for credentials before passing a request further into our routing stack:
These are also defined in our configuration and passed to Twilio in our webhook configuration URLs:
One thing that trips up new users of Twilio is the concept that Twilio will attempt to fetch a URL you define at runtime when processing an inbound phone call to your Twilio number.
Our service therefore responds on the defined endpoint with valid Twilio Markup Language describing the exact combination of speech and sounds (such as angry and ear-piercing cat screeches) that callers should experience:
By writing sane Golang, we’ve built a service that is robust and unlikely to fail for common and avoidable reasons such as mishandling data types. We now need a deployment strategy that will ensure maximum uptime so that our pranking service is always available when we need it.
First we Dockerize our service, such that we have a single isolate containing all code and configuration necessary to run our service:
Normally, I would have the resulting image be devoid of configuration information so that everything could be specified at runtime via environment variables and so that the build artifacts would be truly universally usable. For the sake of this example application and simplification, I baked the config file into the container.
Further optimizations that I may add in the future would include leveraging multi-stage Docker builds to achieve the maximum layer caching benefit and to tremendously slim down the ultimate runtime container to just a few Megabytes: containing our single statically-linked Golang binary and nothing else.
Now that we have a Docker image, we can define a Kubernetes deployment and service that will handle keeping our service running and exposing it to the internet so that Twilio can reach it:
With our service running in Kubernetes, we can monitor it by streaming logs via kubectl, but mostly rest easy knowing that our cluster will restart our service should it fall over for any reason.
We’ve designed and implemented a clean, reliable service in Golang that allows us to prank our friends and family at will.
Questions? Comments? Leave them below and thanks for reading.