How to get rid of the annoying firewall prompt on 'go run'

Published 09.12.2022 • Last modified 21.01.2024

TLDR: #

http.ListenAndServe(":8080", nil) // bad
http.ListenAndServe("localhost:8080", nil) // good

If you are using Go with Windows, you might encounter a small annoyance when running a http server. When using go run to run the server, Windows Defender will sometimes prompt you for firewall access. Here is an example of a Gin server doing it:

Go Gin server example showing Windows Defender Firewall prompt

package main

import (
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	r.Run() // listen and serve on :8080
}

As you can see, we run the server with (*gin.Engine).Run(addr ...string), but aren’t specifying an address. (*gin.Engine).Run() will call http.ListenAndServe with the default address of “:8080”. The default address only contains the port, not the IP address. Because of this Go’s address resolver will default to using the IP “0.0.0.0”, which is just the host’s network IP address. This means that other devices on the network can connect to it.

You can also access the http server with “localhost:8080”, which is simply an alias for “127.0.0.1:8080”. This is a special address, the host loopback address. Connections to “127.0.0.1” will never leave the computer, but instead “loop back”.

Because we are binding the server to the “0.0.0.0” IP, which is accessible by other devices on the network, the firewall is going to warn us. Why does it ask for permission every time you run the server though?

Go Gin server example showing Windows Defender Firewall prompt

Looking back at the firewall prompt, we can see that the path is somewhat unexpected. The reason is that go run . creates a new temporary directory for every build. Windows Defender firewall is smart enough to remember permissions for individual paths, but not for a randomly generated one. You can get around this by running go build -o server.exe && server.exe, which will output the executable in the same path every time.

Binding to localhost only #

A better alternative is to specify the address as “localhost:8080”. This way the server is not visible to the network and therefore won’t invoke the firewall at all.

package main

import (
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	r.Run("localhost:8080")
}

Now the annoying firewall prompt will not appear anymore. You can access the server by navigating to “localhost:8080” or “127.0.0.1:8080”, as they are the same thing. Though you won’t be able to use the host’s IP like you could previously.

A good thing to remember about this approach is that unless your server is behind a reverse proxy, it will not be able to accept outside connections when deployed to production. You can test this by trying to access the server from the host’s IP (which you can see with ipconfig)