Solutions
The starting solution can be found here.
The final solution can be found here.
Overview
In this lesson we want to create a new endpoint on a new port that, when we hit it, indicates that we want to transition from one protocol to another. It’s extremely simple, we don’t have to convey any information, just the mere fact that we hit the endpoint is interpreted to mean: “transition from the current protocol to the other protocol”.
What We’ll Create
- Control API (
./internals/control/control_api.go
)
Global Flag System
There are numerous ways for us to implement this system. Here I’ll opt for what’s probably the simplest - which is just a global Boolean flag. Just imagine conceptually that this flag is false
by default, and if we hit a specific endpoint using a specific method it’ll change to true
.
Now, as we saw before, usually our server will just always respond with either false
(for HTTPS) or 42.42.42.42
(for DNS). It’s currently hardcoded, there’s no consideration to follow an alternative avenue.
But imagine that instead of just responding with the given value it first checks this global flag. BTW, global in this context means “accessible from anywhere in the application”, in other languages it’s sometimes also termed as being public
.
It checks the global flag, if the flag is false
(i.e. we did not hit the endpoint) it will indeed just response with false
/42.42.42.42
.
But, if the flag is true
(i.e. we did hit the endpoint signalling our desire to change protocol) then our server will instead response with true
(for HTTPS) or 69.69.69.69
(for DNS).
In this lesson we’re implementing a mechanism to allow us to trigger a new type of response from our server to the agent. Then, all we need to do in the remaining lessons is ensure that our agent can take variable actions based on that information.
One final thing, one additional layer of nuance, we need to consider is this: I just said if we hit an endpoint the flag changes from false
to true
, and if the server sees that the flag is true
it will send the “change” response to the agent.
But when we hit the endpoint, we don’t want it to continuously change back and forth, we want it to only change a single time. Then, perhaps later if we hit the endpoint again, we want it to change again. But we don’t want it either be stuck in “don’t change” versus “change each time” mode.
In other words, we want to hit the endpoint, we want the flag to change to true
, but then if the server sees it’s true
and changes its response to the agent accordingly, the flag should of course reset to false
. This is known as a “consume once” pattern, and is very simple: if the server detects the flag is true
, change it’s response to the agent AND reset the flag to false
.
Create our Control API
Let’s start implementing all the logic we just discussed in a new file ./internals/control/control_api.go
First thing, let’s create our global flag. Now this could just be a boolean, but a much better practice would be to create a struct so that we could pair a boolean with a mutex.
Let’s define the struct
, and instantiate a global instance of it by capitalizing the name.
// TransitionManager handles the global transition state
type TransitionManager struct {
mu sync.RWMutex
shouldTransition bool
}
// Global instance
var Manager = &TransitionManager{
shouldTransition: false,
}
Next we’ll create a method that will change the value from false to true. That’s all it does, and of course this is the method that will be called by the handler when we hit our endpoint.
// TriggerTransition sets the transition flag
func (tm *TransitionManager) TriggerTransition() {
tm.mu.Lock()
defer tm.mu.Unlock()
tm.shouldTransition = true
log.Printf("Transition triggered")
}
We now need our method that our server can call to:
- Check the value of
shouldTransition
, - Reset the value to
false
if it istrue
.
// CheckAndReset atomically checks if transition is needed and resets the flag
// This ensures the transition signal is consumed only once
func (tm *TransitionManager) CheckAndReset() bool {
tm.mu.Lock()
defer tm.mu.Unlock()
if tm.shouldTransition {
tm.shouldTransition = false // Reset immediately
log.Printf("Transition signal consumed and reset")
return true
}
return false
}
Let’s create a simple HTTP server (we’ll use port 8080) that will expose an endpoint for us to hit to call the TriggerTransition()
method.
// StartControlAPI starts the control API server on port 8080
func StartControlAPI() {
http.HandleFunc("/switch", handleSwitch)
log.Println("Starting Control API on :8080")
go func() {
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Printf("Control API error: %v", err)
}
}()
}
As you can see I’ve chosen the endpoint /switch
. Further, we’re not actually calling TriggerTransition()
here, but as with all endpoints we’re calling a handler handleSwitch
, which will be tasked with calling the method in turn.
Last thing, let’s implement our handler handleSwitch
:
func handleSwitch(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
Manager.TriggerTransition()
response := "Protocol transition triggered"
json.NewEncoder(w).Encode(response)
}
I’ve arbitrarily choses to limit the endpoint to POST method requests only just to show you how to do this, but of course we could leave this out and allow the use of any method.
So our Control API Client is complete, but now of course we need to rewrite both our HTTPS and DNS handlers so that instead of being hardcoded to response false
/42.42.42.42
, they should use the CheckAndReset()
method we created.
DNS Handler Changes
Let’s first change our DNS Handler. Right now we have this single line that just says - always respond with 42.42.42.42
:
A: net.ParseIP("42.42.42.42"),
So we’ll remove this, and then make a few changes to allow for conditional logic:
// handleDNSRequest is our DNS Server's handler
func (s *DNSServer) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
// Create response message
m := new(dns.Msg)
m.SetReply(r)
m.Authoritative = true
// Process each question
for _, question := range r.Question {
// We only handle A records for now
if question.Qtype != dns.TypeA {
continue
}
// Log the query
log.Printf("DNS query for: %s", question.Name)
// NEW LOGIC STARTS HERE
shouldTransition := control.Manager.CheckAndReset()
var responseIP string
if shouldTransition {
responseIP = "69.69.69.69"
log.Printf("DNS: Sending transition signal (69.69.69.69)")
} else {
responseIP = "42.42.42.42"
log.Printf("DNS: Normal response (42.42.42.42)")
}
// Create the response with the appropriate IP
rr := &dns.A{
Hdr: dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: 300,
},
A: net.ParseIP(responseIP), // <-- Using variable instead of hardcoded
}
m.Answer = append(m.Answer, rr)
}
// Send response
w.WriteMsg(m)
}
We create shouldTransition
, which receives the return value of our exposed method CheckAndReset()
. We then use an if
/else
statement to assign, based on the value of shouldTransition
either 69.69.69.69
(if true
), or 42.42.42.42
(if false
) to the variable responseIP
.
And then when we get to A, we use this variable, instead of a hardcoded value. EZ PZ.
HTTPS Handler Changes
The change to our HTTP Handler is even simpler since we can just directly assign the value of the global flag to the field in our JSON. This is of course because we are sending a boolean value, and it should be the same as the flag - if the flag value is true
, our Change
field is equal to true
; and vice-versa.
So replace this:
// Create response with change set to false
response := HTTPSResponse{
Change: false,
}
With this:
// Check if we should transition
shouldChange := control.Manager.CheckAndReset()
response := HTTPSResponse{
Change: shouldChange,
}
if shouldChange {
log.Printf("HTTPS: Sending transition signal (change=true)")
} else {
log.Printf("HTTPS: Normal response (change=false)")
}
For posterity’s sake I’ll include the entire function here:
func RootHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Endpoint %s has been hit by agent\n", r.URL.Path)
// Check if we should transition
shouldChange := control.Manager.CheckAndReset()
response := HTTPSResponse{
Change: shouldChange,
}
if shouldChange {
log.Printf("HTTPS: Sending transition signal (change=true)")
} else {
log.Printf("HTTPS: Normal response (change=false)")
}
// Set content type to JSON
w.Header().Set("Content-Type", "application/json")
// Encode and send the response
if err := json.NewEncoder(w).Encode(response); err != nil {
log.Printf("Error encoding response: %v\n", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
}
Start Control API Server in Server’s main
Finally, we just need to ensure we call the constructor to instantiate our API server in our Server’s main
function. Let’s place it between loading our config, and instantiating our actual server.
// Load configuration
cfg, err := config.LoadConfig(*configPath)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// Load our control API
control.StartControlAPI()
// Create server using interface's factory function
server, err := models.NewServer(cfg)
if err != nil {
log.Fatalf("Failed to create server: %v", err)
}
Test - HTTPS
Just a reminder that we’re not yet expecting our actual agent to transition, all we want to see is that hitting our client will indeed change the global flag, which will lead to our server sending true
/69.69.69.69
, and that our agent will now display this value.
Let’s first test HTTPS - so make sure the protocol
field in config.yaml
is set to https
.
Once both are running, hit our control API endpoint with the following command:
curl -X POST http://localhost:8080/switch
Which will then trigger the output:
❯ curl -X POST http://localhost:8080/switch
"Protocol transition triggered"
Now let’s have a look at the output on the server:
❯ go run ./cmd/server
2025/08/24 09:12:44 Starting Control API on :8080
2025/08/24 09:12:44 Starting https server on 127.0.0.1:8443
2025/08/24 09:13:10 Endpoint / has been hit by agent
2025/08/24 09:13:10 HTTPS: Normal response (change=false)
2025/08/24 09:13:11 Transition triggered
2025/08/24 09:13:14 Endpoint / has been hit by agent
2025/08/24 09:13:14 Transition signal consumed and reset
2025/08/24 09:13:14 HTTPS: Sending transition signal (change=true)
We can see that:
- We did indeed start our Control API on 8080
- Our server was initially hit by the agent, we responded with
false
- The Control API endpoint was hit initiation the transition (
Transition triggered
) - Thereafter, when the agent hit the endpoint, it “
consumed the signal and reset
” (CheckAndReset()
) - The server then sent
change=true
We can confirm this on the agent’s end:
❯ go run ./cmd/agent
2025/08/24 09:13:10 Starting https client run loop
2025/08/24 09:13:10 Delay: 5s, Jitter: 50%
2025/08/24 09:13:10 Received response: change=false
2025/08/24 09:13:10 Sleeping for 4.796768665s
2025/08/24 09:13:14 Received response: change=true
Great, let’s do the same thing for DNS, so just change the protocol field value to dns
in config.yaml
Test - DNS
Do the exact same thing - start the server, start the agent, then hit our Control API endpoint:
❯ curl -X POST http://localhost:8080/switch
"Protocol transition triggered"
We see the same essential logic play out on the server:
❯ go run ./cmd/server
2025/08/24 09:18:34 Starting Control API on :8080
2025/08/24 09:18:34 Starting dns server on 127.0.0.1:8443
2025/08/24 09:18:42 DNS query for: www.thisdoesnotexist.com.
2025/08/24 09:18:42 DNS: Normal response (42.42.42.42)
2025/08/24 09:18:43 Transition triggered
2025/08/24 09:18:50 DNS query for: www.thisdoesnotexist.com.
2025/08/24 09:18:50 Transition signal consumed and reset
2025/08/24 09:18:50 DNS: Sending transition signal (69.69.69.69)
And on the agent
❯ go run ./cmd/agent
2025/08/24 09:18:42 Starting dns client run loop
2025/08/24 09:18:42 Delay: 5s, Jitter: 50%
2025/08/24 09:18:42 Sending DNS query for: www.thisdoesnotexist.com.
2025/08/24 09:18:42 Received DNS response: www.thisdoesnotexist.com. -> 42.42.42.42
2025/08/24 09:18:42 Received response: IP=42.42.42.42
2025/08/24 09:18:42 Sleeping for 7.213587574s
2025/08/24 09:18:50 Sending DNS query for: www.thisdoesnotexist.com.
2025/08/24 09:18:50 Received DNS response: www.thisdoesnotexist.com. -> 69.69.69.69
2025/08/24 09:18:50 Received response: IP=69.69.69.69
2025/08/24 09:18:50 Sleeping for 6.293953329s