Multiplex http and ssh ports

This commit is contained in:
Tom Wiesing 2023-03-08 11:27:19 +01:00
parent 668f1dd193
commit f0073a649f
No known key found for this signature in database
20 changed files with 188 additions and 29 deletions

View file

@ -21,6 +21,7 @@ import (
// Config contains many methods that do not require any interaction with any running components.
// Methods that require running components are instead store inside the [Distillery] or an appropriate [Component].
type Config struct {
Listen ListenConfig `yaml:"listen" recurse:"true"`
Paths PathsConfig `yaml:"paths" recurse:"true"`
HTTP HTTPConfig `yaml:"http" recurse:"true"`
Theme ThemeConfig `yaml:"theme" recurse:"true"`
@ -37,9 +38,6 @@ type Config struct {
// This variable can be used to determine their length.
PasswordLength int `yaml:"password_length" default:"64" validate:"positive"`
// Public port to use for the ssh server
PublicSSHPort uint16 `yaml:"ssh_port" default:"2222" validate:"port"`
// session secret holds the secret for login
SessionSecret string `yaml:"session_secret" validate:"nonempty" sensitive:"true"`

View file

@ -1,3 +1,12 @@
listen:
# A list of ports the distillery should accept traffic on.
# Each of these ports accepts http, https and ssh traffic via a multiplexer.
ports: null
# The ssh port that is shown to the user in various interfaces.
# This port is not automatically included in the ports to listen to.
advertise_ssh: null
paths:
# A WissKI Distillery needs to store a lot of data on disk.
# This setting defines a root folder all of these will be placed in.

View file

@ -23,6 +23,14 @@ type HTTPConfig struct {
CertbotEmail string `yaml:"certbot_email" validate:"email"`
}
// TCPMuxCommand generates a command line for the sslh executable.
func (hcfg HTTPConfig) TCPMuxCommand(addr string, http string, https string, ssh string) string {
if hcfg.HTTPSEnabled() {
return fmt.Sprintf("-bind %s -http %s -tls %s -rest %s", addr, http, https, ssh)
}
return fmt.Sprintf("-bind %s -http %s -rest %s", addr, http, ssh)
}
// HTTPSEnabled returns if the distillery has HTTPS enabled, and false otherwise.
func (hcfg HTTPConfig) HTTPSEnabled() bool {
return hcfg.CertbotEmail != ""

View file

@ -84,7 +84,11 @@ func (legacy *Legacy) Migrate(cfg *config.Config) error {
cfg.TS.DataPrefix = legacy.GraphDBRepoPrefix
cfg.SQL.Database = legacy.DistilleryDatabase
cfg.PasswordLength = legacy.PasswordLength
cfg.PublicSSHPort = legacy.PublicSSHPort
cfg.Listen.Ports = []uint16{80, legacy.PublicSSHPort}
if legacy.CertbotEmail != "" {
cfg.Listen.Ports = append(cfg.Listen.Ports, 443)
}
cfg.Listen.AdvertisedSSHPort = legacy.PublicSSHPort
cfg.TS.AdminUsername = legacy.TriplestoreAdminUser
cfg.TS.AdminPassword = legacy.TriplestoreAdminPassword
cfg.SQL.AdminUsername = legacy.MysqlAdminUser

33
internal/config/ports.go Normal file
View file

@ -0,0 +1,33 @@
package config
import (
"fmt"
"golang.org/x/exp/slices"
)
type ListenConfig struct {
// Ports are the public addresses to bind to.
// Each address is automatically multiplexed to serve http, https and ssh traffic.
// This should typically be port 80 and port 443.
Ports []uint16 `yaml:"ports" default:"80" validate:"ports"`
// AdvertisedSSHPort is the port that shows up as the ssh port in various places in the interface.
// It is automaticalled added to the ports to listen to.
AdvertisedSSHPort uint16 `yaml:"advertise_ssh" default:"80" validate:"port"`
}
// ComposePorts returns a list of ports to be used within a docker-compose.yml file.
// These can be used to forward all ports to the internal port.
func (lc ListenConfig) ComposePorts(internal string) []string {
// sort and uniquify ports
ports := append([]uint16{lc.AdvertisedSSHPort}, lc.Ports...)
slices.Sort(ports)
ports = slices.Compact(ports)
forwards := make([]string, len(ports))
for i, port := range ports {
forwards[i] = fmt.Sprintf("%d:%s", port, internal)
}
return forwards
}

View file

@ -79,6 +79,10 @@ func (tpl *Template) SetDefaults() (err error) {
// Generate generates a configuration file for this configuration
func (tpl Template) Generate() Config {
return Config{
Listen: ListenConfig{
Ports: []uint16{80},
AdvertisedSSHPort: 80,
},
Paths: PathsConfig{
Root: tpl.RootPath,
OverridesJSON: filepath.Join(tpl.RootPath, bootstrap.OverridesJSON),
@ -114,8 +118,6 @@ func (tpl Template) Generate() Config {
MaxBackupAge: 30 * 24 * time.Hour, // 1 month
PasswordLength: 64,
PublicSSHPort: 2222,
SessionSecret: tpl.SessionSecret,
CronInterval: 10 * time.Minute,
}

View file

@ -21,6 +21,7 @@ func New() validator.Collection {
validator.Add(coll, "positive", ValidatePositive)
validator.Add(coll, "port", ValidatePort)
validator.AddSlice(coll, "ports", ",", ValidatePort)
validator.Add(coll, "duration", ValidateDuration)
return coll

View file

@ -59,7 +59,7 @@ func (panel *UserPanel) sshRoute(ctx context.Context) http.Handler {
}
sc.Domain = panel.Config.HTTP.PrimaryDomain
sc.Port = panel.Config.PublicSSHPort
sc.Port = panel.Config.Listen.AdvertisedSSHPort
// pick the first domain that the user has access to as an example
grants, err := panel.Dependencies.Policy.User(r.Context(), user.User.User)

View file

@ -0,0 +1 @@
DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}

View file

@ -0,0 +1,88 @@
package binder
import (
"bytes"
"embed"
"io"
"path/filepath"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/tkw1536/pkglib/yamlx"
"gopkg.in/yaml.v3"
)
type Binder struct {
component.Base
}
var (
_ component.Installable = (*Binder)(nil)
)
func (binder *Binder) Path() string {
return filepath.Join(binder.Still.Config.Paths.Root, "core", "binder")
}
func (binder *Binder) Context(parent component.InstallationContext) component.InstallationContext {
return parent
}
//go:embed docker-compose.yml
var composeTemplate []byte
func (binder *Binder) buildYML() ([]byte, error) {
var dockerCompose yaml.Node
if err := yaml.Unmarshal(composeTemplate, &dockerCompose); err != nil {
return nil, err
}
for dockerCompose.Kind == yaml.DocumentNode {
dockerCompose = *dockerCompose.Content[0]
}
{
ports := binder.Config.Listen.ComposePorts("8000")
portsNode, err := yamlx.Marshal(ports)
if err != nil {
return nil, err
}
if err := yamlx.Replace(&dockerCompose, *portsNode, "services", "binder", "ports"); err != nil {
return nil, err
}
}
{
command := binder.Config.HTTP.TCPMuxCommand("0.0.0.0:8000", "http:80", "http:443", "ssh:2222")
commandNode, err := yamlx.Marshal(command)
if err != nil {
return nil, err
}
if err := yamlx.Replace(&dockerCompose, *commandNode, "services", "binder", "command"); err != nil {
return nil, err
}
}
// do the final marshal
return yaml.Marshal(dockerCompose)
}
//go:embed binder.env
var resources embed.FS
func (binder *Binder) Stack() component.StackWithResources {
return component.MakeStack(binder, component.StackWithResources{
Resources: resources,
EnvPath: "binder.env",
ReadComposeFile: func() (io.Reader, error) {
data, err := binder.buildYML()
if err != nil {
return nil, err
}
return bytes.NewReader(data), nil
},
EnvContext: map[string]string{
"DOCKER_NETWORK_NAME": binder.Config.Docker.Network,
},
})
}

View file

@ -0,0 +1,17 @@
version: "3.7"
services:
binder:
image: ghcr.io/fau-cdi/tcpmux
# dynamically generated
command: []
# dynamically generated
ports: []
restart: always
networks:
- default
networks:
default:
name: ${DOCKER_NETWORK_NAME}
external: true

View file

@ -82,7 +82,7 @@ func (ssh2 *SSH2) handleConnection(session ssh.Session) {
{"${SLUG}", slug},
{"${DOMAIN}", ssh2.Config.HTTP.PrimaryDomain},
{"${HOSTNAME}", slug + "." + ssh2.Config.HTTP.PrimaryDomain},
{"${PORT}", strconv.FormatUint(uint64(ssh2.Config.PublicSSHPort), 10)},
{"${PORT}", strconv.FormatUint(uint64(ssh2.Config.Listen.AdvertisedSSHPort), 10)},
} {
banner = strings.ReplaceAll(banner, oldnew[0], oldnew[1])
}

View file

@ -6,5 +6,3 @@ SELF_OVERRIDES_FILE=${SELF_OVERRIDES_FILE}
SELF_RESOLVER_BLOCK_FILE=${SELF_RESOLVER_BLOCK_FILE}
DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}
HTTPS_ENABLED=${HTTPS_ENABLED}
SSH_PORT=${SSH_PORT}

View file

@ -1,13 +1,12 @@
version: "3.7"
services:
dis:
ssh:
read_only: true
build: .
restart: always
environment:
CONFIG_PATH: ${CONFIG_PATH}
ports:
- "${SSH_PORT}:2222"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
- "${CONFIG_PATH}:${CONFIG_PATH}:ro"

View file

@ -3,7 +3,6 @@ package ssh2
import (
"embed"
"path/filepath"
"strconv"
"github.com/FAU-CDI/wisski-distillery/internal/bootstrap"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
@ -17,7 +16,7 @@ func (ssh SSH2) Path() string {
var resources embed.FS
func (ssh *SSH2) Stack() component.StackWithResources {
stt := component.MakeStack(ssh, component.StackWithResources{
return component.MakeStack(ssh, component.StackWithResources{
Resources: resources,
ContextPath: "ssh2",
EnvPath: "ssh2.env",
@ -25,20 +24,16 @@ func (ssh *SSH2) Stack() component.StackWithResources {
EnvContext: map[string]string{
"DOCKER_NETWORK_NAME": ssh.Config.Docker.Network,
"HOST_RULE": ssh.Config.HTTP.DefaultHostRule(),
"HTTPS_ENABLED": ssh.Config.HTTP.HTTPSEnabledEnv(),
"CONFIG_PATH": ssh.Config.ConfigPath,
"DEPLOY_ROOT": ssh.Config.Paths.Root,
"SELF_OVERRIDES_FILE": ssh.Config.Paths.OverridesJSON,
"SELF_RESOLVER_BLOCK_FILE": ssh.Config.Paths.ResolverBlocks,
"SSH_PORT": strconv.FormatUint(uint64(ssh.Config.PublicSSHPort), 10),
},
CopyContextFiles: []string{bootstrap.Executable},
})
return stt
}
func (ssh SSH2) Context(parent component.InstallationContext) component.InstallationContext {

View file

@ -192,6 +192,10 @@ func (is StackWithResources) Install(ctx context.Context, progress io.Writer, co
); err != nil {
return err
}
} else {
if err := fsx.MkdirAll(is.Dir, fsx.DefaultDirPerm); err != nil {
return err
}
}
// write the docker-compose.yml file

View file

@ -1,7 +1,7 @@
version: "3.7"
services:
reverse-proxy:
http:
image: docker.io/library/traefik:v2.9
command:
- "--providers.docker"
@ -12,9 +12,9 @@ services:
## for debugging purposes, the following can be enabled.
# - "--api.insecure=true"
ports:
- "80:80"
# - "127.0.0.1:8888:8080"
#ports:
# # - "80:80"
# # - "127.0.0.1:8888:8080"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
restart: always

View file

@ -1,7 +1,7 @@
version: "3.7"
services:
reverse-proxy:
http:
image: docker.io/library/traefik:v2.9
command:
- "--providers.docker"
@ -24,10 +24,10 @@ services:
# - "--api.insecure=true"
# - "--certificatesresolvers.distillery.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
ports:
- "80:80"
- "443:443"
# - "127.0.0.1:8888:8080"
#ports:
# # - "80:80"
# # - "443:443"
# # - "127.0.0.1:8888:8080"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
- "./acme.json:/acme.json"

View file

@ -11,6 +11,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/next"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/panel"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/policy"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/binder"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/docker"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter/logger"
@ -131,6 +132,7 @@ func (dis *Distillery) Purger() *purger.Purger {
func (dis *Distillery) allComponents() []initFunc {
return []initFunc{
auto[*docker.Docker],
auto[*binder.Binder],
auto[*web.Web],
manual(func(ts *triplestore.Triplestore) {