Files
unkin-agent af55d83de6
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Port the launcher to Go on the podman API bindings
2026-10-24 16:49:43 +11:00

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;
}
}
}
`