181 lines
4.4 KiB
Go
181 lines
4.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
// Every .domain entry resolves here; exact hosts get 127.1.x.y of their own.
|
|
wildcardIP = "127.0.0.2"
|
|
portBase = 10000
|
|
)
|
|
|
|
var hostRE = regexp.MustCompile(`^\.?[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$`)
|
|
|
|
// entry is one allowlist line; port is empty for plain web (80/443) access.
|
|
type entry struct{ host, port string }
|
|
|
|
// parseAllowlist returns the entries of allowlist file path, sorted bytewise
|
|
// on "host port" and unique.
|
|
func parseAllowlist(path string) ([]entry, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
|
|
seen := map[string]bool{}
|
|
var keys []string
|
|
sc := bufio.NewScanner(f)
|
|
for n := 1; sc.Scan(); n++ {
|
|
line, _, _ := strings.Cut(sc.Text(), "#")
|
|
fields := strings.Fields(line)
|
|
if len(fields) == 0 {
|
|
continue
|
|
}
|
|
if len(fields) > 1 {
|
|
return nil, fmt.Errorf("%s:%d: one entry per line", path, n)
|
|
}
|
|
line = strings.ToLower(fields[0])
|
|
host, port := line, ""
|
|
if i := strings.LastIndex(line, ":"); i >= 0 {
|
|
host, port = line[:i], line[i+1:]
|
|
}
|
|
if !hostRE.MatchString(host) {
|
|
return nil, fmt.Errorf("%s:%d: invalid host '%s'", path, n, host)
|
|
}
|
|
if port != "" {
|
|
p, err := strconv.Atoi(port)
|
|
if err != nil || strings.ContainsAny(port, "+-") || p < 1 || p > 65535 {
|
|
return nil, fmt.Errorf("%s:%d: invalid port '%s'", path, n, port)
|
|
}
|
|
if strings.HasPrefix(host, ".") {
|
|
return nil, fmt.Errorf("%s:%d: ports need an exact host, not '%s'", path, n, host)
|
|
}
|
|
port = strconv.Itoa(p)
|
|
if p == 80 || p == 443 {
|
|
port = ""
|
|
}
|
|
}
|
|
if k := host + " " + port; !seen[k] {
|
|
seen[k] = true
|
|
keys = append(keys, k)
|
|
}
|
|
}
|
|
if err := sc.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
sort.Strings(keys)
|
|
entries := make([]entry, len(keys))
|
|
for i, k := range keys {
|
|
entries[i].host, entries[i].port, _ = strings.Cut(k, " ")
|
|
}
|
|
return entries, nil
|
|
}
|
|
|
|
// routerConfig renders dnsmasq.conf and nginx.conf for the router listening on
|
|
// rip, and the sandbox's loopback forwards as "bind:port:router:router_port"
|
|
// words.
|
|
func routerConfig(entries []entry, rip string) (dnsmasq, nginx, forwards string) {
|
|
var hosts []string
|
|
isHost := map[string]bool{}
|
|
for _, e := range entries {
|
|
if !isHost[e.host] {
|
|
isHost[e.host] = true
|
|
hosts = append(hosts, e.host)
|
|
}
|
|
}
|
|
ip := map[string]string{}
|
|
i := 0
|
|
for _, h := range hosts {
|
|
if !strings.HasPrefix(h, ".") {
|
|
ip[h] = fmt.Sprintf("127.1.%d.%d", i/250, i%250+1)
|
|
i++
|
|
}
|
|
}
|
|
|
|
var tcp strings.Builder
|
|
fwd := []string{"0.0.0.0:443:" + rip + ":443", "0.0.0.0:80:" + rip + ":80"}
|
|
j := 0
|
|
for _, e := range entries {
|
|
if e.port == "" {
|
|
continue
|
|
}
|
|
fmt.Fprintf(&tcp, " server { listen %s:%d; set $up %s:%s; proxy_pass $up; }\n", rip, portBase+j, e.host, e.port)
|
|
fwd = append(fwd, fmt.Sprintf("%s:%s:%s:%d", ip[e.host], e.port, rip, portBase+j))
|
|
j++
|
|
}
|
|
|
|
var d strings.Builder
|
|
fmt.Fprintf(&d, "no-resolv\nno-hosts\nbind-interfaces\nlisten-address=%s\naddress=/#/\n", rip)
|
|
for _, h := range hosts {
|
|
if strings.HasPrefix(h, ".") {
|
|
fmt.Fprintf(&d, "address=/%s/%s\n", h[1:], wildcardIP)
|
|
} else {
|
|
fmt.Fprintf(&d, "host-record=%s,%s\n", h, ip[h])
|
|
}
|
|
}
|
|
|
|
// nginx "hostnames" rejects an exact key that a .domain key already covers.
|
|
var m strings.Builder
|
|
for _, h := range hosts {
|
|
if !strings.HasPrefix(h, ".") && isHost["."+h] {
|
|
continue
|
|
}
|
|
fmt.Fprintf(&m, " %s 1;\n", h)
|
|
}
|
|
|
|
nginx = strings.NewReplacer("@RIP@", rip, "@MAP@", m.String(), "@TCP@", tcp.String()).Replace(nginxTemplate)
|
|
return d.String(), nginx, strings.Join(fwd, " ")
|
|
}
|
|
|
|
// @RESOLVER@ is filled in by the router's start.sh.
|
|
const nginxTemplate = `load_module /usr/lib/nginx/modules/ngx_stream_module.so;
|
|
worker_processes 1;
|
|
pid /tmp/nginx.pid;
|
|
error_log stderr warn;
|
|
events { worker_connections 1024; }
|
|
|
|
stream {
|
|
resolver @RESOLVER@ ipv6=off valid=30s;
|
|
map $ssl_preread_server_name $tls_allowed {
|
|
hostnames;
|
|
default 0;
|
|
@MAP@
|
|
}
|
|
map $tls_allowed $tls_upstream {
|
|
1 $ssl_preread_server_name:443;
|
|
default 127.0.0.1:9;
|
|
}
|
|
server {
|
|
listen @RIP@:443;
|
|
ssl_preread on;
|
|
proxy_pass $tls_upstream;
|
|
}
|
|
@TCP@}
|
|
|
|
http {
|
|
resolver @RESOLVER@ ipv6=off valid=30s;
|
|
access_log off;
|
|
map $host $http_allowed {
|
|
hostnames;
|
|
default 0;
|
|
@MAP@
|
|
}
|
|
server {
|
|
listen @RIP@:80;
|
|
if ($http_allowed = 0) { return 403; }
|
|
location / {
|
|
proxy_pass http://$host;
|
|
proxy_set_header Host $host;
|
|
}
|
|
}
|
|
}
|
|
`
|