Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"log"
"time"

"github.com/doubleunion/accesscontrol/router"
rpio "github.com/stianeikeland/go-rpio/v4"
Expand All @@ -14,5 +15,18 @@ func main() {
}
defer rpio.Close()

// Run updateIPAndRestart every minute in a separate thread
go func() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small suggestion – it might be cleaner to move this to the router module, and put all the logic around it in a new file

e.g. dynamic_ip.go might have:

// starts the go routine that calls lookup every second, could optionally implement a `stop` but probably never need it. started from within `RunRouter`
func startPolling()

// Looks up the latest IP for `doubleunion.tplinkdns.com` and assigns it to `localInternetAddress`
func lookupIP()

ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()

for range ticker.C {
err := router.UpdateIPAndRestart()
if err != nil {
log.Printf("Error in updateIPAndRestart: %v", err)
}
}
}()

router.RunRouter()
}
70 changes: 70 additions & 0 deletions router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"net"
"net/http"
"os"
"os/exec"
"strings"
"time"

"github.com/doubleunion/accesscontrol/door"
Expand All @@ -19,6 +21,9 @@ import (
"golang.org/x/crypto/acme/autocert"
)

const serviceFilePath = "/etc/systemd/system/accesscontrol.service"
const ipQueryURL = "https://wtfismyip.com/text"

var localInternetAddress = os.Getenv("LOCAL_INTERNET_ADDRESS")

func RunRouter() {
Expand Down Expand Up @@ -111,3 +116,68 @@ func requireLocalNetworkMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return next(c)
}
}

func UpdateIPAndRestart() error {
// Step 1: Query current IP address
resp, err := http.Get(ipQueryURL)

@Imperiopolis Imperiopolis Jul 2, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rather than relying on a 3rd party API, I think this could be simplified to do a DNS lookup, something like:

ips, err := net.LookupIP("doubleunion.tplinkdns.com")
...(error handling)...
fmt.Printf("IP address: %s\n", ips[0].String())

There should always just be a single IPv4 address in the result ([135.180.39.34]), based on how the dyndns is setup, but we could also update localInternetAddress to be an array that we check against in requireLocalNetworkMiddleware to future proof in case at some point in the future we start getting IPv6 or multiple addresses assigned

if err != nil {
return err
}
defer resp.Body.Close()

ipBytes, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
currentIP := strings.TrimSpace(string(ipBytes))

// Step 2: Read the service file
content, err := os.ReadFile(serviceFilePath)
if err != nil {
return err
}

// Step 3: Check if the IP matches
lines := strings.Split(string(content), "\n")
var updatedContent []string
ipUpdated := false
for _, line := range lines {
if strings.HasPrefix(line, "Environment=LOCAL_INTERNET_ADDRESS=") {
fileIP := strings.TrimPrefix(line, "Environment=LOCAL_INTERNET_ADDRESS=")
if fileIP != currentIP {
line = "Environment=LOCAL_INTERNET_ADDRESS=" + currentIP
ipUpdated = true
}
}
updatedContent = append(updatedContent, line)
}

// Step 4: Update the file if necessary
if ipUpdated {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of editing the accesscontrol.service and rebooting the pi, it might be cleaner to just update the localInternetAddress variable and stop caching the IP in a file / reading it from ENV altogether

If there's still a compelling reason for a cache, it still might be cleaner to make a new file that we write just the IP to, and that we read it from at each app startup (instead of from ENV). Then instead of rebooting, you can just exit and wait for the service to restart the process.

// first we have to output the new contents to a temporary file
// because we don't have access to the service file directly
tempFilePath := "/tmp/accesscontrol.service"
err = os.WriteFile(tempFilePath, []byte(strings.Join(updatedContent, "\n")), 0644)
if err != nil {
return err
}

// then we copy the temporary file to the service file path
// the path is owned by the process user so this is allowed by the OS without sudo
cmd := exec.Command("cp", tempFilePath, serviceFilePath)
err = cmd.Run()
if err != nil {
return err
}

// Step 5: Restart the Raspberry Pi
cmd = exec.Command("sudo", "shutdown", "-r", "now")
//log.Printf("Error in updateIPAndRestart: %v", err)
err = cmd.Run()
if err != nil {
return err
}
}

return nil
}