>Synopsis:      relayd mixes up filter rules when a protocol is reused in 
>multiple relays
>Category:      system
>Environment:
        System      : OpenBSD 6.6
        Details     : OpenBSD 6.6 (GENERIC.MP) #372: Sat Oct 12 10:56:27 MDT 
2019
                         
[email protected]:/usr/src/sys/arch/amd64/compile/GENERIC.MP

        Architecture: OpenBSD.amd64
        Machine     : amd64
>Description:
        I set up relayd in front of httpd, so that http://$(hostname)/* should 
redirect to
        https://$(hostname)/*, https://$(hostname)/ is the main site but 
https://$(hostname)/app is a
        embedded webapp. This sort of reverse proxy set up is common with 
webapps (like NextCloud,
        Mattermost, Gitea, Radicale, etc) to avoid having to fight the 
same-origin policy: it allows
        /app and the main site to communicate via cookies, websockets, etc. It 
also avoids firewalls
        that otherwise block webapps run on unusual ports (e.g. the way cPanel 
is usually installed)
        by making all apps share a single port: 443.

        But relayd applies the /app proxy I asked of it *to the http:// site*, 
which means people can
        get at my webapp without going through TLS.

        # Afterthought

        After working with this for a bit it's obvious to me that relayd was 
not meant for doing this
        kind of sub-domain/sub-path reverse proxying. What I really want is 
httpd's location blocks,
        which are simple and easy, and importantly feature the `strip` command, 
but to be able to apply
        them to a TCP proxy and not just to FastCGI.
>How-To-Repeat:

0) Make an empty test folder
```
$ mkdir t1; cd t1
```

1) Set up httpd with two vhosts plus a "bounce to https". The first vhost is 
our main site, the second, running on a different port, represents a backend 
webapp that runs its own HTTP server 

```
$ mkdir -p site app
$ mkdir -p logs # httpd insists on this
$ echo "Static Site" > site/index.html
$ echo "WebApp" > app/index.html
$ cat > httpd.conf <<EOF
# httpd.conf
                                                                                
                        
chroot "."

server "default" {
        listen on localhost port 8080
        location * {
            block return 302 "https://\$HTTP_HOST\$REQUEST_URI";
        }
}

server "site" {
        listen on localhost port 8081
        root "site"
        directory auto index
}

server "app" {
        listen on localhost port 8082
        root "app"
        request strip 1 # this site will be mounted at /app but relayd doesn't 
have (obvious?) URL rewriting
        directory auto index
}
EOF
```

```
$ doas httpd -f httpd.conf # httpd demands root, even though it doesn't need it 
in this case
```

Test:
```
$ curl http://localhost:8081/         
Static Site
$ curl http://localhost:8082/
WebApp
```

2) Make a cert for TLS

This will be used to set up relayd listening on two ports -- http and https -- 
and demonstrate
 
This makes a self-signed cert so it'll have to `curl --cacert $(hostname).pem` 
(or just `curl -k`) to use it; if you have a live server you can experiment on, 
use acme-client to get a "real" cert, or reuse a valid cert you have.

```
$ openssl req -x509 -newkey rsa:4096 -subj '/CN='"$(hostname)" -nodes -keyout 
$(hostname).key -out $(hostname).pem -days 3
Generating a 4096 bit RSA private key
...................................................................++++
................................................................................................................................++++
writing new private key to 'matriculate.lan.key'
-----
```

# relayd config

3) Set up relayd with rules to rewrite the URLs.

```
$ cat > relayd.conf <<EOF
table <web> { "127.0.0.1" }
table <app> { "127.0.0.1" }

http protocol web {
        # Return HTTP/HTML error pages to the client
        return error

        tls keypair $(hostname)

        #match request path "/app{,/*}" tag app # want this, but this glob 
syntax isn't supported?
        match request path "/app" tag app
        match request path "/app/*" tag app
        match request tagged app header set "X-Script-Name" value "/app" # has 
to match the above!
        match request tagged app forward to <app>
}

# http:// should bounce to the "default" vhost, which should bounce the client 
to https://
relay http_proxy {
        listen on * port 80

        protocol web
        forward to <web> port 8080
}

# https:// should handle
relay https_proxy { 
        listen on * port 443 tls

        protocol web
        forward to <web> port 8081
        forward to <app> port 8082
}
EOF
```


relayd insists you need the certs to be global:

```
$ doas cp $(pwd)/$(hostname).pem /etc/ssl/$(hostname).crt
$ doas cp $(pwd)/$(hostname).key /etc/ssl/private/$(hostname).key
```


# Testing

In one shell:

```
$ doas relayd -d -v -f relayd.conf                        
startup
adding 1 hosts from table web:8080 (no check)
adding 1 hosts from table web:8080 (no check)
adding 1 hosts from table web:8080 (no check)
adding 1 hosts from table web:8081 (no check)
adding 1 hosts from table web:8081 (no check)
adding 1 hosts from table app:8082 (no check)
adding 1 hosts from table app:8082 (no check)
adding 1 hosts from table web:8081 (no check)
adding 1 hosts from table app:8082 (no check)
```

in a parallel shell:

a) demonstrate that http://$(hostname)/, https://$(hostname)/ and 
https://$(hostname)/app are working as expected

```
$ curl -v http://$(hostname)/
*   Trying 127.0.0.1:80...
* TCP_NODELAY set
* Connected to matriculate.lan (127.0.0.1) port 80 (#0)
> GET / HTTP/1.1
> Host: matriculate.lan
> User-Agent: curl/7.66.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 302 Found
< Connection: close
< Content-Length: 419
< Content-Type: text/html
< Date: Mon, 10 Feb 2020 02:19:00 GMT
< Location: https://matriculate.lan/
< Server: OpenBSD httpd
< 
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>302 Found</title>
<style type="text/css"><!--
body { background-color: white; color: black; font-family: 'Comic Sans MS', 
'Chalkboard SE', 'Comic Neue', sans-serif; }
hr { border: 0; border-bottom: 1px dashed; }

--></style>
</head>
<body>
<h1>302 Found</h1>
<hr>
<address>OpenBSD httpd</address>
</body>
</html>
* Closing connection 0

$ curl --cacert /etc/ssl/$(hostname).crt https://$(hostname)/    
Static Site
$ curl --cacert /etc/ssl/$(hostname).crt https://$(hostname)/app/
WebApp
```

b) But http://$(hostname)/app is *wrong*: it passes through directly to the 
backend without hitting the 302 Redirect:

```
$ curl http://$(hostname)/app/    
WebApp
```

The WebApp, like the Static site, are supposed to be *inaccessible* over http.

So what went wrong here?


Appendix: repro v2 (without TLS)
--------------------------------

The bug doesn't depend on TLS, it was just a bit more realistic and motivating 
to demonstrate it that way.
It still appears if we replace https://$(hostname)/ with 
http://$(hostname):8080/

```
$ mkdir t2; cd t2
$ mkdir -p site app
$ mkdir -p logs # httpd insists on this
$ echo "Static Site" > site/index.html
$ echo "WebApp" > app/index.html
$
$ cat > httpd.conf <<EOF                                                        
                              
chroot "."

server "site" {
        listen on localhost port 8081
        root "site"
        directory auto index
}

server "app" {
        listen on localhost port 8082
        root "app"
        request strip 1
        directory auto index
}
EOF
$ doas httpd -f httpd.conf
$
$ cat > relayd.conf <<EOF                                                       
                                                  
table <web> { "127.0.0.1" }
table <app> { "127.0.0.1" }

http protocol web {
        # Return HTTP/HTML error pages to the client
        return error

        #match request path "/app{,/*}" tag app # want this, but this glob 
syntax isn't supported?
        match request path "/app" tag app
        match request path "/app/*" tag app
        match request tagged app forward to <app>
}

relay http_proxy {
        listen on 0.0.0.0 port 80

        protocol web
        forward to <web> port 8081
}

relay http_alt_proxy { 
        listen on 0.0.0.0 port 8080

        protocol web
        forward to <web> port 8081
        forward to <app> port 8082
}
EOF
$ doas relayd -f relayd.conf
```

Then:

```
$ curl http://$(hostname)/app/ 
WebApp
```

even though the port 80 relay isn't supposed to be forwarding to the webapp.



If we add more "webapps":

```
$ mkdir app2; echo "WebApp 2" > app2/index.html
$ cat >> httpd.conf <EOF

server "app2" {
        listen on localhost port 8083
        root "app2"
        request strip 1
        directory auto index
}
EOF
$ vi relayd.conf # ... edit to say:
$ cat relayd.conf
table <web> { "127.0.0.1" }
table <app> { "127.0.0.1" }
table <app2> { "127.0.0.1" }

http protocol web {
        # Return HTTP/HTML error pages to the client
        return error

        #match request path "/app{,/*}" tag app # want this, but this glob 
syntax isn't supported?
        match request path "/app" tag app
        match request path "/app/*" tag app
        match request tagged app forward to <app>

        #match request path "/app2{,/*}" tag app2 # want this, but this glob 
syntax isn't supported?
        match request path "/app2" tag app2
        match request path "/app2/*" tag app2
        match request tagged app2 forward to <app2>
}

relay http_proxy {
        listen on 0.0.0.0 port 80

        protocol web
        forward to <web> port 8081
}

relay http_alt_proxy {
        listen on 0.0.0.0 port 8080

        protocol web
        forward to <web> port 8081
        forward to <app> port 8082
        forward to <app2> port 8083
}
$ doas pkill httpd
$ doas pkill relayd
$ doas httpd -f httpd.conf
$ doas relayd -f relayd.conf
```

The problem continues:

```
$ curl http://$(hostname):8080/  # expected
Static Site
$ curl http://$(hostname):8080/app/   # expected
WebApp
$ curl http://$(hostname):8080/app2/   # expected
WebApp 2
$ 
$ curl http://$(hostname)/   # expected
Static Site
$ curl http://$(hostname)/app/ # BUG
WebApp
$ curl http://$(hostname)/app2/ # BUG
WebApp 2
```


>Fix:
        Adding this to relayd.conf:

```
http protocol web80 {
        return error
}
```

and making `relay http_proxy` use `protocol web80` makes the problem go away.
But if you can't reuse protocols why distinguish them from relays at all?

Reply via email to