Debug Golang in VS Code in Linux as root

It's quite a mouthful title, so let's explain the problem first. Recently I was working on a Go project related to VPN in Linux, which among other things needs to install a tun interface, so it needs to be sudo-ed (run as root). But since I was logged in as a regular user, every attempt to debug the code in VS Code crashed at the line which creates the tun interface. Nothing surprising there - root user privileges are needed for creating an interface. OK, but how to start debugging as root? Actually, there are probably several solutions, some being trivial:

  • Login to Linux as root user. Many resources online explain how to allow that. Here's one such article for Ubuntu 20.04. But I didn't want to do so. There's a reason why logging as root is disabled in the first place.
  • Launch VS Code as root (something like sudo code .). Honestly, I haven't even tried that, but it kinda makes sense that it works - if a process runs as root, all processes launched by it also run as root, at least I believe so. But again, I would rather avoid launching third-party apps as root unless I have to. Also, chances are that it would cause headaches with environment configuration - VS Code, since running as root, won't be able to see your environment variables. Again, I haven't even tried, so it may happen that I'm wrong - pls correct me.

Solution

After some research, I've found a solution I was satisfied with. It's explained in this Github issue. It is based on executing dlv (a Go debugger) as root. Here's what you need to do:

1. Allow sudo Without Password

To avoid password prompts when using sudo, you have (at least) two options:

  • Pipe the password to sudo, by executing something like (note -S flag):
    echo my_passw0rd | sudo -S dlv
    The drawback of this approach is obvious: you need to have your root password in plain text in your code, which isn't a good idea.
  • Allow sudo without password for particular user/command, by editing sudoers file. So you need to execute sudo visudo, and append the following line at the end of the file:
    yourusername ALL=(root)NOPASSWD:/home/yourusername/go/bin/dlv
    You should replace yourusername with your actual username, of course. Also keep in mind that this line should be added at the end of the sudoers fil, because sudo evaluates the file in order, and takes the last match.

We'll go with the second approach because it's more secure. The added line basically says "when yourusername executes sudo /home/yourusername/go/bin/dlv, don't ask for a password."

2. Create a dlv Wrapper

The next thing we want to do is to intercept all dlm calls, and to prepend them with sudo if needed to accomplish that we need to create dlv-sudo.sh file in .vscode directory of the project, with the following content:

#!/bin/sh
if ! which dlv ; then
	PATH="${GOPATH}/bin:$PATH"
fi
if [ "$DEBUG_AS_ROOT" = "true" ]; then
	DLV=$(which dlv)
	exec sudo "$DLV" --only-same-user=false "$@"
else
	exec dlv "$@"
fi

Don't forget to make the file executable by executing something like: chmod +x .vscode/dlv-sudo.sh

Let's quickly explain the key parts of this script:

  • In line 5 we are checking if we should elevate at all, by examining DEBUG_AS_ROOT environment variable.
  • If the elevation is requested, we need to replace dlv command with the full path (/home/yourusername/go/bin/dlv), because we've defined the full path in the sudoers file. This is exactly what we do in line 6.
  • In line 7 we're executing the elevated (sudo-ed) dlv.
  • If the elevation isn't requested, we're executing dlv in a regular way, in line 9.

3. Make VS Code Use the Wrapper

Once we have the wrapper created, we need to tell VS Code not to use dlv command anymore, but to replace it with our script (with dlv-sudo.sh). To do so, we need to create settings.json file in .vscode directory, with the following content:

{
	"go.alternateTools": {
		"dlv": "${workspaceFolder}/.vscode/dlv-sudo.sh"
	}
}

4. Launch Configuration

OK, we've created the feature, so let's see how we can use it. We are now able to define at launch configuration level if we need debugging as root or not. Here's an example of launch configurations (.vscode/launch.json file), which contains two configurations - one requires root debugging, while another doesn't require:

{
	"version": "0.2.0",
	"configurations": [
		{
			"name": "Run as root",
			"type": "go",
			"request": "launch",
			"mode": "auto",
			"program": "${workspaceFolder}",
			"env": {
				"DEBUG_AS_ROOT": "true",
			}, 
			"args": []
		},
		{
			"name": "Run as myself",
			"type": "go",
			"request": "launch",
			"mode": "auto",
			"program": "${workspaceFolder}",
			"env": {}, 
			"args": []
		}
	]
}

As you can see, the only difference between the two configurations is that the first one, Run as root, defines "DEBUG_AS_ROOT": "true" environment variable, while the second one doesn't. Now we can simply select the desired configuration in VS Code and debug!

Troubleshooting

Even if you do everything as explained above, it may happen that it doesn't work for some reason. For example, in my case it failed with the following error in VS Code DEBUG CONSOLE:

exec: "go": executable file not found in $PATH
Process exiting with code: 1

It took me more than an hour to resolve because I wasn't reading carefully. The error is actually quite descriptive - it says that go command cannot be found. But how is it possible if you can execute it? For example, if you try to get the go version, you'll get something like:

$ go version
go version go1.14.3 linux/amd64

Obviously go works, but still VS Code displays the error saying that go can't be found. What's the reason for such strange behavior? The reason in my case was because GOROOT environment variable was defined in my ~/.profile file, as well as code which adds it to PATH. It means that go command was added to my PATH, but not in root user's PATH. To confirm that this is the reason, I've executed the following:

$ sudo go version
sudo: go: command not found

Sure enough. So the solution is obvious: ensure that root user's PATH also includes go.

Finally, in case of some other problems, there's one potential solution that comes from the Windows world: try with logout/login or restart.