Repository: juanfont/headscale

Issue:
[Bug] Internal server error on /verify
### Is this a support request?

- [x] This is not a support request

### Is there an existing issue for this?

- [x] I have searched the existing issues

### Current Behavior

An internal server error is displayed when non-JSON is sent to `/verify`.

### Expected Behavior

Either respond with HTTP/400 BadRequest or HTTP/200 and `{"Allow":false}` when malformed request data is sent by a client.

### Steps To Reproduce

1. Compile main, currently 30cec3aa
2. Send invalid requests with curl:
   ```console
    curl $HEADSCALE_URL/verify -d ''
    curl $HEADSCALE_URL/verify -d 'a'
    curl $HEADSCALE_URL/verify -d '{'
   ```
3. Observe logs from Headscale
    ```console
    ERR http internal server error error="cannot parse derpAdmitClientRequest: unexpected end of JSON input" code=50
    ERR http internal server error error="cannot parse derpAdmitClientRequest: invalid character 'a' looking for beginning of value" code=500
    ERR http internal server error error="cannot parse derpAdmitClientRequest: unexpected end of JSON input" code=500
   ```

### Environment

```markdown
- OS:Debian 13
- Headscale version: main, 30cec3aa
- Tailscale version: -
```

### Runtime environment

- [ ] Headscale is behind a (reverse) proxy
- [ ] Headscale runs in a container

### Debug information

-

Hints from issue comments:
@nblock: Sending valid JSON but with malformed values also triggers the error:

```console
 curl $HEADSCALE_URL/verify --json '{"nodePublic": "nodekey:this-is-too-short", "source": "192.168.1.1"}'
```

Logs:

```
ERR http internal server error error="cannot parse derpAdmitClientRequest: key hex has the wrong size, got 17 want 64" code=500
```@nblock: Fixed via 086fcad7d958e1f94b172e2dd25f89ccdc3e1955

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 50866950..e3957b80 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -55,7 +55,8 @@ systemctl start headscale
   - **IMPORTANT: Backup your SQLite database before upgrading**
   - Introduces safer table renaming migration strategy
   - Addresses longstanding database integrity issues
-
+- Add support for `autogroup:member`, `autogroup:tagged`
+  [#2572](https://github.com/juanfont/headscale/pull/2572)
 - Remove policy v1 code [#2600](https://github.com/juanfont/headscale/pull/2600)
 - Refactor Debian/Ubuntu packaging and drop support for Ubuntu 20.04.
   [#2614](https://github.com/juanfont/headscale/pull/2614)
@@ -229,8 +230,6 @@ working in v1 and not tested might be broken in v2 (and vice versa).
   [#2438](https://github.com/juanfont/headscale/pull/2438)
 - Add documentation for routes
   [#2496](https://github.com/juanfont/headscale/pull/2496)
-- Add support for `autogroup:member`, `autogroup:tagged`
-  [#2572](https://github.com/juanfont/headscale/pull/2572)
 
 ## 0.25.1 (2025-02-25)
 
diff --git a/docs/ref/acls.md b/docs/ref/acls.md
index 63f83ae2..d74fea6c 100644
--- a/docs/ref/acls.md
+++ b/docs/ref/acls.md
@@ -9,9 +9,38 @@ When using ACL's the User borders are no longer applied. All machines
 whichever the User have the ability to communicate with other hosts as
 long as the ACL's permits this exchange.
 
-## ACLs use case example
+## ACL Setup
 
-Let's build an example use case for a small business (It may be the place where
+To enable and configure ACLs in Headscale, you need to specify the path to your ACL policy file in the `policy.path` key in `config.yaml`.
+
+Your ACL policy file must be formatted using [huJSON](https://github.com/tailscale/hujson).
+
+Info on how these policies are written can be found
+[here](https://tailscale.com/kb/1018/acls/).
+
+Please reload or restart Headscale after updating the ACL file. Headscale may be reloaded either via its systemd service
+(`sudo systemctl reload headscale`) or by sending a SIGHUP signal (`sudo kill -HUP $(pidof headscale)`) to the main
+process. Headscale logs the result of ACL policy processing after each reload.
+
+## Simple Examples
+
+- [**Allow All**](https://tailscale.com/kb/1192/acl-samples#allow-all-default-acl): If you define an ACL file but completely omit the `"acls"` field from its content, Headscale will default to an "allow all" policy. This means all devices connected to your tailnet will be able to communicate freely with each other.
+
+    ```json
+    {}
+    ```
+
+- [**Deny All**](https://tailscale.com/kb/1192/acl-samples#deny-all): To prevent all communication within your tailnet, you can include an empty array for the `"acls"` field in your policy file.
+
+    ```json
+    {
+      "acls": []
+    }
+    ```
+
+## Complex Example
+
+Let's build a more complex example use case for a small business (It may be the place where
 ACL's are the most useful).
 
 We have a small company with a boss, an admin, two developers and an intern.
@@ -38,10 +67,6 @@ servers.
 
 ![ACL implementation example](../images/headscale-acl-network.png)
 
-## ACL setup
-
-ACLs have to be written in [huJSON](https://github.com/tailscale/hujson).
-
 When [registering the servers](../usage/getting-started.md#register-a-node) we
 will need to add the flag `--advertise-tags=tag:<tag1>,tag:<tag2>`, and the user
 that is registering the server should be allowed to do it. Since anyone can add
@@ -49,14 +74,6 @@ tags to a server they can register, the check of the tags is done on headscale
 server and only valid tags are applied. A tag is valid if the user that is
 registering it is allowed to do it.
 
-To use ACLs in headscale, you must edit your `config.yaml` file. In there you will find a `policy.path` parameter. This
-will need to point to your ACL file. More info on how these policies are written can be found
-[here](https://tailscale.com/kb/1018/acls/).
-
-Please reload or restart Headscale after updating the ACL file. Headscale may be reloaded either via its systemd service
-(`sudo systemctl reload headscale`) or by sending a SIGHUP signal (`sudo kill -HUP $(pidof headscale)`) to the main
-process. Headscale logs the result of ACL policy processing after each reload.
-
 Here are the ACL's to implement the same permissions as above:
 
 ```json title="acl.json"
diff --git a/docs/usage/connect/android.md b/docs/usage/connect/android.md
index 98305bd7..b6fa3a66 100644
--- a/docs/usage/connect/android.md
+++ b/docs/usage/connect/android.md
@@ -6,9 +6,23 @@ This documentation has the goal of showing how a user can use the official Andro
 
 Install the official Tailscale Android client from the [Google Play Store](https://play.google.com/store/apps/details?id=com.tailscale.ipn) or [F-Droid](https://f-droid.org/packages/com.tailscale.ipn/).
 
-## Configuring the headscale URL
+## Connect via normal, interactive login
 
 - Open the app and select the settings menu in the upper-right corner
 - Tap on `Accounts`
 - In the kebab menu icon (three dots) in the upper-right corner select `Use an alternate server`
 - Enter your server URL (e.g `https://headscale.example.com`) and follow the instructions
+- The client connects automatically as soon as the node registration is complete on headscale. Until then, nothing is
+  visible in the server logs.
+
+## Connect using a preauthkey
+
+- Open the app and select the settings menu in the upper-right corner
+- Tap on `Accounts`
+- In the kebab menu icon (three dots) in the upper-right corner select `Use an alternate server`
+- Enter your server URL (e.g `https://headscale.example.com`). If login prompts open, close it and continue
+- Open the settings menu in the upper-right corner
+- Tap on `Accounts`
+- In the kebab menu icon (three dots) in the upper-right corner select `Use an auth key`
+- Enter your [preauthkey generated from headscale](../getting-started.md#using-a-preauthkey)
+- If needed, tap `Log in` on the main screen. You should now be connected to your headscale.
diff --git a/hscontrol/handlers.go b/hscontrol/handlers.go
index d6d32e6d..2d664104 100644
--- a/hscontrol/handlers.go
+++ b/hscontrol/handlers.go
@@ -91,7 +91,7 @@ func (h *Headscale) handleVerifyRequest(
 
 	var derpAdmitClientRequest tailcfg.DERPAdmitClientRequest
 	if err := json.Unmarshal(body, &derpAdmitClientRequest); err != nil {
-		return fmt.Errorf("cannot parse derpAdmitClientRequest: %w", err)
+		return NewHTTPError(http.StatusBadRequest, "Bad Request: invalid JSON", fmt.Errorf("cannot parse derpAdmitClientRequest: %w", err))
 	}
 
 	nodes, err := h.state.ListNodes()
diff --git a/hscontrol/policy/v2/filter.go b/hscontrol/policy/v2/filter.go
index c546eb20..ecd8f83e 100644
--- a/hscontrol/policy/v2/filter.go
+++ b/hscontrol/policy/v2/filter.go
@@ -21,7 +21,7 @@ func (pol *Policy) compileFilterRules(
 	users types.Users,
 	nodes views.Slice[types.NodeView],
 ) ([]tailcfg.FilterRule, error) {
-	if pol == nil {
+	if pol == nil || pol.ACLs == nil {
 		return tailcfg.FilterAllowAll, nil
 	}
 



Test patch:
diff --git a/hscontrol/policy/v2/policy_test.go b/hscontrol/policy/v2/policy_test.go
index a91831ad..0140653e 100644
--- a/hscontrol/policy/v2/policy_test.go
+++ b/hscontrol/policy/v2/policy_test.go
@@ -40,8 +40,8 @@ func TestPolicyManager(t *testing.T) {
 			name:         "empty-policy",
 			pol:          "{}",
 			nodes:        types.Nodes{},
-			wantFilter:   nil,
-			wantMatchers: []matcher.Match{},
+			wantFilter:   tailcfg.FilterAllowAll,
+			wantMatchers: matcher.MatchesFromFilterRules(tailcfg.FilterAllowAll),
 		},
 	}