diff --git a/docs/legacy/world-pkg-openvpn/README.md b/docs/legacy/world-pkg-openvpn/README.md new file mode 100644 index 0000000..c4c5c58 --- /dev/null +++ b/docs/legacy/world-pkg-openvpn/README.md @@ -0,0 +1,18 @@ +# world pkg/openvpn — preserved reference implementation + +Source: `github.com/bborbe/world` (repo deleted 2026-07-21 after full migration +to bw), final commit `929ac5a`. Preserved verbatim before deletion. + +This is the Go mini-CA + OpenVPN provisioning code that originally built and +managed the VPN now owned by `bundles/openvpn` + `bundles/openvpn-client`: + +- `server-config.go` — CA/server cert generation (RSA 4096, PKCS1), dhparam, + ta.key; the CA private key stayed on the operator laptop (`~/.openvpn/`) +- `client-config.go` — client cert signing + the client.conf template +- `server.go` / `client-remote.go` / `client-local.go` — deployment logic + (superseded by the bw bundles) +- `openvpn.go` — types (IRoutes/ClientIPs → now node metadata `clients` map) + +Kept for the 2030 cert renewal and as the authoritative answer to "how were +these certs generated". Renewal gotchas are documented in +`bundles/openvpn/README.md`. Not compiled, not imported — reference only. diff --git a/docs/legacy/world-pkg-openvpn/client-config.go b/docs/legacy/world-pkg-openvpn/client-config.go new file mode 100644 index 0000000..bfe19db --- /dev/null +++ b/docs/legacy/world-pkg-openvpn/client-config.go @@ -0,0 +1,261 @@ +// Copyright (c) 2019 Benjamin Borbe All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package openvpn + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "net" + "os" + "os/user" + "path" + "path/filepath" + "time" + + "github.com/bborbe/world/pkg/content" + "github.com/bborbe/world/pkg/file" + "github.com/bborbe/world/pkg/template" + "github.com/bborbe/world/pkg/validation" +) + +type ClientConfig struct { + ClientName ClientName + ServerConfig ServerConfig + ServerAddress ServerAddress + Routes Routes + Device Device +} + +func (c ClientConfig) Validate(ctx context.Context) error { + return validation.Validate( + ctx, + c.ServerConfig.ServerName, + c.ServerConfig.ServerPort, + c.ClientName, + c.ServerAddress, + c.Device, + ) +} + +func (c *ClientConfig) ConfigContent() content.HasContent { + return content.Func(func(ctx context.Context) ([]byte, error) { + port, err := c.ServerConfig.ServerPort.Port(ctx) + if err != nil { + return nil, err + } + + type Route struct { + Gateway string + Net string + Mask string + } + data := struct { + ServerName string + ServerHost string + ServerPort int + Routes []Route + Device string + }{ + ServerName: c.ServerConfig.ServerName.String(), + ServerHost: c.ServerAddress.String(), + ServerPort: port, + Routes: []Route{}, + Device: c.Device.String(), + } + for _, route := range c.Routes { + gateway, err := route.Gateway.IP(ctx) + if err != nil { + return nil, err + } + ipnet, err := route.IPNet.IPNet(ctx) + if err != nil { + return nil, err + } + data.Routes = append(data.Routes, Route{ + Gateway: gateway.String(), + Net: ipnet.IP.String(), + Mask: net.IP(ipnet.Mask).String(), + }) + } + return template.Render(` +#viscosity startonopen true +#viscosity usepeerdns false +#viscosity ipv6 false +#viscosity dns off +#viscosity protocol openvpn +#viscosity autoreconnect true +#viscosity dnssupport true +#viscosity name {{.ServerName}} +#viscosity dhcp false + +client +dev {{.Device}} +proto tcp4 +remote {{.ServerHost}} {{.ServerPort}} +resolv-retry infinite +nobind +persist-key +persist-tun +ca ca.crt +cert client.crt +key client.key +remote-cert-tls client +tls-auth ta.key 1 +cipher AES-256-CBC +# comp-lzo + +verb 3 + +{{range $route := .Routes}} +route {{$route.Net}} {{$route.Mask}} {{$route.Gateway}} +{{ end }} +`, data) + }) +} + +func (c *ClientConfig) localPath(filename string) file.HasPath { + return file.PathFunc(func(ctx context.Context) (string, error) { + directory, err := c.clientDirectory() + if err != nil { + return "", err + } + return path.Join(directory, filename), nil + }) +} + +func (c *ClientConfig) clientDirectory() (string, error) { + usr, err := user.Current() + if err != nil { + return "", fmt.Errorf("get homedir failed: %w", err) + } + dir := filepath.Join(usr.HomeDir, ".openvpn", c.ClientName.String()) + if err := os.MkdirAll(dir, 0700); err != nil { + return "", err + } + return dir, nil +} + +func (c *ClientConfig) ClientKey() content.Func { + return func(ctx context.Context) ([]byte, error) { + caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, err + } + return pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey), + }), nil + } +} + +func (c *ClientConfig) ClientCertifcate() *x509.Certificate { + return &x509.Certificate{ + SerialNumber: big.NewInt(1658), + Subject: pkix.Name{ + CommonName: c.ClientName.String(), + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + SubjectKeyId: []byte{1, 2, 3, 4, 6}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature, + } +} + +func (c *ClientConfig) ClientCrt() content.Func { + return func(ctx context.Context) ([]byte, error) { + caPriv, err := readLocal(ctx, c.ServerConfig.LocalPathCAPrivateKey()) + if err != nil { + return nil, err + } + + caPrivPem, _ := pem.Decode(caPriv) + if caPrivPem.Type != "RSA PRIVATE KEY" { + return nil, fmt.Errorf("invalid type %s", caPrivPem.Type) + } + + caPrivKey, err := x509.ParsePKCS1PrivateKey(caPrivPem.Bytes) + if err != nil { + return nil, err + } + + certKey, err := readLocal(ctx, c.LocalPathClientKey()) + if err != nil { + return nil, err + } + + certPrivPem, _ := pem.Decode(certKey) + if certPrivPem.Type != "RSA PRIVATE KEY" { + return nil, fmt.Errorf("invalid type %s", certPrivPem.Type) + } + + certPrivKey, err := x509.ParsePKCS1PrivateKey(certPrivPem.Bytes) + if err != nil { + return nil, err + } + + certBytes, err := x509.CreateCertificate( + rand.Reader, + c.ClientCertifcate(), + c.ServerConfig.CACertifcate(), + &certPrivKey.PublicKey, + caPrivKey, + ) + if err != nil { + return nil, err + } + + return pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + }), nil + } +} + +func (c *ClientConfig) CaCrt() content.Func { + return func(ctx context.Context) (bytes []byte, err error) { + path, err := c.ServerConfig.LocalPathCaCrt().Path(ctx) + if err != nil { + return nil, err + } + return os.ReadFile(path) + } +} + +func (c *ClientConfig) TAKey() content.Func { + return func(ctx context.Context) (bytes []byte, err error) { + path, err := c.ServerConfig.LocalPathTaKey().Path(ctx) + if err != nil { + return nil, err + } + return os.ReadFile(path) + } +} + +func (c *ClientConfig) LocalPathTaKey() file.HasPath { + return c.localPath("ta.key") +} + +func (c *ClientConfig) LocalPathCaCrt() file.HasPath { + return c.localPath("ca.crt") +} + +func (c *ClientConfig) LocalPathClientKey() file.HasPath { + return c.localPath("client.key") +} + +func (c *ClientConfig) LocalPathClientCrt() file.HasPath { + return c.localPath("client.crt") +} + +func (c *ClientConfig) LocalPathConfig() file.HasPath { + return c.localPath("client.ovpn") +} diff --git a/docs/legacy/world-pkg-openvpn/client-local.go b/docs/legacy/world-pkg-openvpn/client-local.go new file mode 100644 index 0000000..5a10dfc --- /dev/null +++ b/docs/legacy/world-pkg-openvpn/client-local.go @@ -0,0 +1,88 @@ +// Copyright (c) 2019 Benjamin Borbe All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package openvpn + +import ( + "context" + + "github.com/bborbe/world/pkg/local" + "github.com/bborbe/world/pkg/network" + "github.com/bborbe/world/pkg/validation" + "github.com/bborbe/world/pkg/world" +) + +type LocalClient struct { + ClientName ClientName + ServerName ServerName + ServerAddress ServerAddress + Routes Routes + ServerPort network.Port + Device Device +} + +func (l *LocalClient) Validate(ctx context.Context) error { + return validation.Validate( + ctx, + l.ClientName, + l.ServerName, + l.ServerAddress, + l.ServerPort, + l.clientConfig(), + l.Device, + ) +} + +func (l *LocalClient) Children(ctx context.Context) (world.Configurations, error) { + clientConfig := l.clientConfig() + return world.Configurations{ + world.NewConfiguraionBuilder().WithApplier( + &local.FileContent{ + Path: clientConfig.LocalPathConfig(), + Content: clientConfig.ConfigContent(), + }, + ), + world.NewConfiguraionBuilder().WithApplier( + &local.FileContent{ + Path: clientConfig.LocalPathCaCrt(), + Content: clientConfig.CaCrt(), + }, + ), + world.NewConfiguraionBuilder().WithApplier( + &local.FileContent{ + Path: clientConfig.LocalPathTaKey(), + Content: clientConfig.TAKey(), + }, + ), + world.NewConfiguraionBuilder().WithApplier( + &local.FileContent{ + Path: clientConfig.LocalPathClientKey(), + Content: clientConfig.ClientKey(), + }, + ), + world.NewConfiguraionBuilder().WithApplier( + &local.FileContent{ + Path: clientConfig.LocalPathClientCrt(), + Content: clientConfig.ClientCrt(), + }, + ), + }, nil +} + +func (l *LocalClient) Applier() (world.Applier, error) { + return nil, nil +} + +func (l *LocalClient) clientConfig() ClientConfig { + return ClientConfig{ + ClientName: l.ClientName, + ServerAddress: l.ServerAddress, + ServerConfig: ServerConfig{ + ServerName: l.ServerName, + ServerPort: l.ServerPort, + }, + Routes: l.Routes, + Device: l.Device, + } +} diff --git a/docs/legacy/world-pkg-openvpn/client-remote.go b/docs/legacy/world-pkg-openvpn/client-remote.go new file mode 100644 index 0000000..0948712 --- /dev/null +++ b/docs/legacy/world-pkg-openvpn/client-remote.go @@ -0,0 +1,146 @@ +// Copyright (c) 2019 Benjamin Borbe All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package openvpn + +import ( + "context" + + "github.com/bborbe/world/configuration/service" + "github.com/bborbe/world/pkg/apt" + "github.com/bborbe/world/pkg/file" + "github.com/bborbe/world/pkg/network" + "github.com/bborbe/world/pkg/remote" + "github.com/bborbe/world/pkg/ssh" + "github.com/bborbe/world/pkg/validation" + "github.com/bborbe/world/pkg/world" +) + +type RemoteClient struct { + SSH *ssh.SSH + ClientName ClientName + ServerName ServerName + ServerAddress ServerAddress + ServerPort network.Port + Routes Routes + Device Device +} + +func (r *RemoteClient) Validate(ctx context.Context) error { + return validation.Validate( + ctx, + r.SSH, + r.ClientName, + r.ServerName, + r.ServerAddress, + r.ServerPort, + r.clientConfig(), + r.Device, + ) +} + +func (r *RemoteClient) Children(ctx context.Context) (world.Configurations, error) { //nolint:funlen + clientConfig := r.clientConfig() + return world.Configurations{ + &service.Directory{ + SSH: r.SSH, + Path: file.Path("/etc/openvpn"), + User: "root", + Group: "root", + Perm: 0700, + }, + &remote.File{ + SSH: r.SSH, + Path: file.PathFunc(func(ctx context.Context) (string, error) { + return "/etc/openvpn/client.conf", nil + }), + User: "root", + Group: "root", + Perm: 0600, + Content: clientConfig.ConfigContent(), + }, + &remote.FileLocalCached{ + SSH: r.SSH, + Path: file.Path("/etc/openvpn/ca.crt"), + LocalPath: clientConfig.LocalPathCaCrt(), + User: "root", + Group: "root", + Perm: 0600, + Content: clientConfig.CaCrt(), + }, + &remote.FileLocalCached{ + SSH: r.SSH, + Path: file.Path("/etc/openvpn/ta.key"), + LocalPath: clientConfig.LocalPathTaKey(), + User: "root", + Group: "root", + Perm: 0600, + Content: clientConfig.TAKey(), + }, + &remote.FileLocalCached{ + SSH: r.SSH, + Path: file.Path("/etc/openvpn/client.key"), + LocalPath: clientConfig.LocalPathClientKey(), + User: "root", + Group: "root", + Perm: 0600, + Content: clientConfig.ClientKey(), + }, + &remote.FileLocalCached{ + SSH: r.SSH, + Path: file.Path("/etc/openvpn/client.crt"), + LocalPath: clientConfig.LocalPathClientCrt(), + User: "root", + Group: "root", + Perm: 0600, + Content: clientConfig.ClientCrt(), + }, + world.NewConfiguraionBuilder().WithApplier(&apt.Update{ + SSH: r.SSH, + }), + world.NewConfiguraionBuilder().WithApplier(&apt.Install{ + SSH: r.SSH, + Package: "openvpn", + }), + world.NewConfiguraionBuilder().WithApplier(&apt.Autoremove{ + SSH: r.SSH, + }), + world.NewConfiguraionBuilder().WithApplier(&apt.Clean{ + SSH: r.SSH, + }), + &remote.File{ + SSH: r.SSH, + Path: file.Path("/etc/default/openvpn"), + User: "root", + Group: "root", + Perm: 0644, + Content: clientConfig.ServerConfig.OpenvpnDefaultConf(), + }, + world.NewConfiguraionBuilder().WithApplier(&remote.ServiceStart{ + SSH: r.SSH, + Name: "openvpn", + }), + world.NewConfiguraionBuilder().WithApplier(&remote.ServiceStart{ + SSH: r.SSH, + Name: "openvpn@client", + }), + }, nil +} + +func (r *RemoteClient) Applier() (world.Applier, error) { + return nil, nil +} + +func (r *RemoteClient) clientConfig() ClientConfig { + return ClientConfig{ + ClientName: r.ClientName, + ServerAddress: r.ServerAddress, + ServerConfig: ServerConfig{ + ServerName: r.ServerName, + ServerPort: r.ServerPort, + }, + Routes: r.Routes, + Device: r.Device, + } +} diff --git a/docs/legacy/world-pkg-openvpn/openvpn.go b/docs/legacy/world-pkg-openvpn/openvpn.go new file mode 100644 index 0000000..6b356d5 --- /dev/null +++ b/docs/legacy/world-pkg-openvpn/openvpn.go @@ -0,0 +1,142 @@ +// Copyright (c) 2019 Benjamin Borbe All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package openvpn + +import ( + "context" + + "github.com/bborbe/errors" + + "github.com/bborbe/world/pkg/network" + "github.com/bborbe/world/pkg/validation" +) + +type Device string + +func (d Device) Validate(ctx context.Context) error { + if d == "" { + return errors.New(ctx, "Device empty") + } + return nil +} + +func (d Device) String() string { + return string(d) +} + +const Tap Device = "tap" + +const Tun Device = "tun" + +type ClientName string + +func (c ClientName) String() string { + return string(c) +} + +func (c ClientName) Validate(ctx context.Context) error { + if c == "" { + return errors.New(ctx, "ClientName empty") + } + return nil +} + +type ServerName string + +func (s ServerName) String() string { + return string(s) +} + +func (s ServerName) Validate(ctx context.Context) error { + if s == "" { + return errors.New(ctx, "ServerName empty") + } + return nil +} + +type ServerAddress string + +func (s ServerAddress) String() string { + return string(s) +} + +func (s ServerAddress) Validate(ctx context.Context) error { + if s == "" { + return errors.New(ctx, "ServerName empty") + } + return nil +} + +type IRoute struct { + Name ClientName + IPNet network.IPNet +} + +func (r IRoutes) Validate(ctx context.Context) error { + for _, route := range r { + if err := route.Validate(ctx); err != nil { + return err + } + } + return nil +} + +type IRoutes []IRoute + +func (r IRoute) Validate(ctx context.Context) error { + return validation.Validate( + ctx, + r.IPNet, + r.Name, + ) +} + +type Routes []Route + +func (r Routes) Validate(ctx context.Context) error { + for _, route := range r { + if err := route.Validate(ctx); err != nil { + return err + } + } + return nil +} + +type Route struct { + Gateway network.IP + IPNet network.IPNet +} + +func (r Route) Validate(ctx context.Context) error { + return validation.Validate( + ctx, + r.IPNet, + r.Gateway, + ) +} + +type ClientIPs []ClientIP + +func (c ClientIPs) Validate(ctx context.Context) error { + for _, clientIP := range c { + if err := clientIP.Validate(ctx); err != nil { + return err + } + } + return nil +} + +type ClientIP struct { + Name ClientName + IP network.IP +} + +func (c ClientIP) Validate(ctx context.Context) error { + return validation.Validate( + ctx, + c.Name, + c.IP, + ) +} diff --git a/docs/legacy/world-pkg-openvpn/server-config.go b/docs/legacy/world-pkg-openvpn/server-config.go new file mode 100644 index 0000000..6e0eeb8 --- /dev/null +++ b/docs/legacy/world-pkg-openvpn/server-config.go @@ -0,0 +1,367 @@ +// Copyright (c) 2019 Benjamin Borbe All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package openvpn + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "net" + "os" + "os/exec" + "os/user" + "path" + "path/filepath" + "time" + + "github.com/bborbe/world/pkg/content" + "github.com/bborbe/world/pkg/file" + "github.com/bborbe/world/pkg/network" + "github.com/bborbe/world/pkg/template" + "github.com/bborbe/world/pkg/validation" +) + +type ServerConfig struct { + ServerName ServerName + ServerIPNet network.IPNet + ServerPort network.Port + Routes Routes + Device Device +} + +func (s ServerConfig) Validate(ctx context.Context) error { + return validation.Validate( + ctx, + s.ServerName, + s.ServerIPNet, + s.Routes, + s.ServerPort, + s.Device, + ) +} + +func (s *ServerConfig) OpenvpnDefaultConf() content.Func { + return func(ctx context.Context) ([]byte, error) { + return []byte(` +AUTOSTART="all" +OPTARGS="" +OMIT_SENDSIGS=0 +`), nil + } +} + +func (s *ServerConfig) ServerConfigContent() content.Func { + return func(ctx context.Context) ([]byte, error) { + serverIPNet, err := s.ServerIPNet.IPNet(ctx) + if err != nil { + return nil, err + } + + port, err := s.ServerPort.Port(ctx) + if err != nil { + return nil, err + } + + type Route struct { + Gateway string + Net string + Mask string + } + data := struct { + ServerIP string + ServerNetmask string + ServerPort int + Routes []Route + Device string + }{ + ServerIP: serverIPNet.IP.String(), + ServerNetmask: net.IP(serverIPNet.Mask).String(), + ServerPort: port, + Routes: []Route{}, + Device: s.Device.String(), + } + for _, route := range s.Routes { + gateway, err := route.Gateway.IP(ctx) + if err != nil { + return nil, err + } + ipnet, err := route.IPNet.IPNet(ctx) + if err != nil { + return nil, err + } + data.Routes = append(data.Routes, Route{ + Gateway: gateway.String(), + Net: ipnet.IP.String(), + Mask: net.IP(ipnet.Mask).String(), + }) + } + return template.Render(` +dev {{.Device}} +port {{.ServerPort}} +proto tcp4 +server {{.ServerIP}} {{.ServerNetmask}} +ca /etc/openvpn/keys/ca.crt +cert /etc/openvpn/keys/server.crt +key /etc/openvpn/keys/server.key +dh /etc/openvpn/keys/dh.pem +tls-auth /etc/openvpn/keys/ta.key 0 +ifconfig-pool-persist ip_pool +keepalive 10 120 +cipher AES-256-CBC +persist-key +persist-tun +status server.status +topology subnet +# comp-lzo +client-config-dir /etc/openvpn/ccd +push "route {{.ServerIP}} {{.ServerNetmask}}" +client-to-client + +# route to 192.168.178.0/24 via opnsense +route 192.168.178.0 255.255.255.0 172.16.90.10 + +verb 3 +log /var/log/openvpn/server.log + +{{range $route := .Routes}} +route {{$route.Net}} {{$route.Mask}} {{$route.Gateway}} +{{ end }} +`, data) + } +} + +func (s *ServerConfig) CAPrivateKey() content.Func { + return func(ctx context.Context) ([]byte, error) { + caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, err + } + return pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey), + }), nil + } +} + +func (s *ServerConfig) CACertifcate() *x509.Certificate { + return &x509.Certificate{ + SerialNumber: big.NewInt(2019), + Subject: pkix.Name{ + Organization: []string{"Benjamin Borbe"}, + Country: []string{"DE"}, + Province: []string{"Hessen"}, + Locality: []string{"Wiesbaden"}, + StreetAddress: []string{""}, + PostalCode: []string{""}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageClientAuth, + x509.ExtKeyUsageServerAuth, + }, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } +} + +func (s *ServerConfig) CaCrt() content.Func { + return func(ctx context.Context) ([]byte, error) { + caPriv, err := readLocal(ctx, s.LocalPathCAPrivateKey()) + if err != nil { + return nil, err + } + + caPrivPem, _ := pem.Decode(caPriv) + if caPrivPem.Type != "RSA PRIVATE KEY" { + return nil, fmt.Errorf("invalid type %s", caPrivPem.Type) + } + + caPrivKey, err := x509.ParsePKCS1PrivateKey(caPrivPem.Bytes) + if err != nil { + return nil, err + } + + caCert, err := x509.CreateCertificate( + rand.Reader, + s.CACertifcate(), + s.CACertifcate(), + &caPrivKey.PublicKey, + caPrivKey, + ) + if err != nil { + return nil, err + } + + return pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: caCert, + }), nil + } +} + +func (s *ServerConfig) ServerCertifcate() *x509.Certificate { + return &x509.Certificate{ + SerialNumber: big.NewInt(1658), + Subject: pkix.Name{ + CommonName: s.ServerName.String(), + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + SubjectKeyId: []byte{1, 2, 3, 4, 6}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature, + } +} + +func (s *ServerConfig) ServerCrt() content.Func { + return func(ctx context.Context) ([]byte, error) { + caPriv, err := readLocal(ctx, s.LocalPathCAPrivateKey()) + if err != nil { + return nil, err + } + + caPrivPem, _ := pem.Decode(caPriv) + if caPrivPem.Type != "RSA PRIVATE KEY" { + return nil, fmt.Errorf("invalid type %s", caPrivPem.Type) + } + + caPrivKey, err := x509.ParsePKCS1PrivateKey(caPrivPem.Bytes) + if err != nil { + return nil, err + } + + certKey, err := readLocal(ctx, s.LocalPathServerKey()) + if err != nil { + return nil, err + } + + certPrivPem, _ := pem.Decode(certKey) + if certPrivPem.Type != "RSA PRIVATE KEY" { + return nil, fmt.Errorf("invalid type %s", certPrivPem.Type) + } + + certPrivKey, err := x509.ParsePKCS1PrivateKey(certPrivPem.Bytes) + if err != nil { + return nil, err + } + + certBytes, err := x509.CreateCertificate( + rand.Reader, + s.ServerCertifcate(), + s.CACertifcate(), + &certPrivKey.PublicKey, + caPrivKey, + ) + if err != nil { + return nil, err + } + + return pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + }), nil + } +} + +func (s *ServerConfig) ServerKey() content.Func { + return func(ctx context.Context) ([]byte, error) { + caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, err + } + return pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey), + }), nil + } +} + +func (s *ServerConfig) TAKey() content.Func { + return func(ctx context.Context) ([]byte, error) { + buf := &bytes.Buffer{} + command := exec.CommandContext(ctx, "openvpn2", "--genkey", "--secret", "/dev/stdout") + command.Stdout = buf + err := command.Run() + if err != nil { + return nil, err + } + return buf.Bytes(), nil + } +} + +func (s *ServerConfig) DHPem() content.Func { + return func(ctx context.Context) ([]byte, error) { + buf := &bytes.Buffer{} + command := exec.CommandContext(ctx, "openssl", "dhparam", "-out", "-", "1024") + command.Stdout = buf + err := command.Run() + if err != nil { + return nil, err + } + return buf.Bytes(), nil + } +} + +func (s *ServerConfig) LocalPathCaCrt() file.HasPath { + return s.localPath("ca.crt") +} + +func (s *ServerConfig) LocalPathCAPrivateKey() file.HasPath { + return s.localPath("ca.key") +} + +func (s *ServerConfig) LocalPathServerCrt() file.HasPath { + return s.localPath("server.crt") +} + +func (s *ServerConfig) LocalPathDhPem() file.HasPath { + return s.localPath("dh.pem") +} + +func (s *ServerConfig) LocalPathTaKey() file.HasPath { + return s.localPath("ta.key") +} + +func (s *ServerConfig) LocalPathServerKey() file.HasPath { + return s.localPath("server.key") +} + +func (s *ServerConfig) serverDirectory() (string, error) { + usr, err := user.Current() + if err != nil { + return "", fmt.Errorf("get homedir failed: %w", err) + } + dir := filepath.Join(usr.HomeDir, ".openvpn", s.ServerName.String()) + if err := os.MkdirAll(dir, 0700); err != nil { + return "", err + } + return dir, nil +} + +func (s *ServerConfig) localPath(filename string) file.HasPath { + return file.PathFunc(func(ctx context.Context) (string, error) { + directory, err := s.serverDirectory() + if err != nil { + return "", err + } + return path.Join(directory, filename), nil + }) +} + +func readLocal(ctx context.Context, path file.HasPath) ([]byte, error) { + filename, err := path.Path(ctx) + if err != nil { + return nil, err + } + return os.ReadFile(filename) +} diff --git a/docs/legacy/world-pkg-openvpn/server.go b/docs/legacy/world-pkg-openvpn/server.go new file mode 100644 index 0000000..b95edd9 --- /dev/null +++ b/docs/legacy/world-pkg-openvpn/server.go @@ -0,0 +1,325 @@ +// Copyright (c) 2018 Benjamin Borbe All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package openvpn + +import ( + "bytes" + "context" + "fmt" + "net" + "sort" + + "github.com/bborbe/world/configuration/service" + "github.com/bborbe/world/pkg/apt" + "github.com/bborbe/world/pkg/content" + "github.com/bborbe/world/pkg/file" + "github.com/bborbe/world/pkg/local" + "github.com/bborbe/world/pkg/network" + "github.com/bborbe/world/pkg/remote" + "github.com/bborbe/world/pkg/ssh" + "github.com/bborbe/world/pkg/validation" + "github.com/bborbe/world/pkg/world" +) + +type Server struct { + SSH *ssh.SSH + ServerName ServerName + ServerPort network.Port + ServerIPNet network.IPNet + Routes Routes + IRoutes IRoutes + ClientIPs ClientIPs + Device Device +} + +func (s *Server) Validate(ctx context.Context) error { + return validation.Validate( + ctx, + s.SSH, + s.ServerName, + s.ServerIPNet, + s.ServerPort, + s.Routes, + s.IRoutes, + s.ClientIPs, + s.Device, + ) +} + +func (s *Server) Children(ctx context.Context) (world.Configurations, error) { //nolint:funlen + serverConfig := s.serverConfig() + + configurations := make([]world.Configuration, 0, 20+len(s.IRoutes)) + configurations = append(configurations, []world.Configuration{ + &service.Directory{ + SSH: s.SSH, + Path: file.Path("/etc/openvpn/keys"), + User: "root", + Group: "root", + Perm: 0700, + }, + &service.Directory{ + SSH: s.SSH, + Path: file.Path("/etc/openvpn/ccd"), + User: "root", + Group: "root", + Perm: 0700, + }, + &service.Directory{ + SSH: s.SSH, + Path: file.Path("/var/log/openvpn"), + User: "root", + Group: "root", + Perm: 0700, + }, + &remote.File{ + SSH: s.SSH, + Path: file.Path("/etc/openvpn/server.conf"), + User: "root", + Group: "root", + Perm: 0600, + Content: serverConfig.ServerConfigContent(), + }, + &remote.File{ + SSH: s.SSH, + Path: file.Path("/etc/openvpn/ip_pool"), + User: "root", + Group: "root", + Perm: 0600, + Content: s.ipPoolContent(), + }, + &remote.FileLocalCached{ + SSH: s.SSH, + Path: file.Path("/etc/openvpn/keys/ta.key"), + LocalPath: serverConfig.LocalPathTaKey(), + User: "root", + Group: "root", + Perm: 0600, + Content: serverConfig.TAKey(), + }, + &remote.FileLocalCached{ + SSH: s.SSH, + Path: file.Path("/etc/openvpn/keys/dh.pem"), + LocalPath: serverConfig.LocalPathDhPem(), + User: "root", + Group: "root", + Perm: 0600, + Content: serverConfig.DHPem(), + }, + world.NewConfiguraionBuilder().WithApplier( + &local.FileContent{ + Path: serverConfig.LocalPathCAPrivateKey(), + Content: serverConfig.CAPrivateKey(), + }, + ), + &remote.FileLocalCached{ + SSH: s.SSH, + Path: file.Path("/etc/openvpn/keys/ca.crt"), + LocalPath: serverConfig.LocalPathCaCrt(), + User: "root", + Group: "root", + Perm: 0600, + Content: serverConfig.CaCrt(), + }, + &remote.FileLocalCached{ + SSH: s.SSH, + Path: file.Path("/etc/openvpn/keys/server.key"), + LocalPath: serverConfig.LocalPathServerKey(), + User: "root", + Group: "root", + Perm: 0600, + Content: serverConfig.ServerKey(), + }, + &remote.FileLocalCached{ + SSH: s.SSH, + Path: file.Path("/etc/openvpn/keys/server.crt"), + LocalPath: serverConfig.LocalPathServerCrt(), + User: "root", + Group: "root", + Perm: 0600, + Content: serverConfig.ServerCrt(), + }, + world.NewConfiguraionBuilder().WithApplier(&remote.IptablesAllowInput{ + SSH: s.SSH, + Port: network.PortStatic(563), + Protocol: network.TCP, + }), + world.NewConfiguraionBuilder().WithApplier(&apt.Update{ + SSH: s.SSH, + }), + world.NewConfiguraionBuilder().WithApplier(&apt.Install{ + SSH: s.SSH, + Package: "openvpn", + }), + world.NewConfiguraionBuilder().WithApplier(&apt.Autoremove{ + SSH: s.SSH, + }), + world.NewConfiguraionBuilder().WithApplier(&apt.Clean{ + SSH: s.SSH, + }), + &remote.File{ + SSH: s.SSH, + Path: file.Path("/etc/default/openvpn"), + User: "root", + Group: "root", + Perm: 0644, + Content: serverConfig.OpenvpnDefaultConf(), + }, + world.NewConfiguraionBuilder().WithApplier(&remote.ServiceStart{ + SSH: s.SSH, + Name: "openvpn", + }), + world.NewConfiguraionBuilder().WithApplier(&remote.ServiceStart{ + SSH: s.SSH, + Name: "openvpn@server", + }), + &service.Sysctl{ + SSH: s.SSH, + Options: service.SysctlOptions{ + { + Option: "net.ipv4.ip_forward", + Value: "1", + }, + }, + }, + world.NewConfiguraionBuilder().WithApplier(&remote.IptablesAllowInput{ + SSH: s.SSH, + Port: serverConfig.ServerPort, + Protocol: network.TCP, + }), + world.NewConfiguraionBuilder().WithApplier(&remote.IptablesAllowForward{ + SSH: s.SSH, + }), + }...) + + // Per-client ccd file: authoritative static tunnel IP (ifconfig-push) plus + // the client's iroute. ifconfig-push is required for a stable VpnIP — + // ifconfig-pool-persist alone is only advisory, so newly-added clients would + // otherwise land on a dynamic pool IP instead of their configured VpnIP. + // topology is "subnet", so the push uses the VPN subnet netmask. + irouteByName := make(map[ClientName]network.IPNet, len(s.IRoutes)) + for _, iroute := range s.IRoutes { + irouteByName[iroute.Name] = iroute.IPNet + } + serverIPNet := s.ServerIPNet + for _, clientIP := range s.ClientIPs { + // iroute is optional: a client with no matching IRoutes entry still gets + // its ifconfig-push (static IP), just no pushed subnet route. Callers build + // ClientIPs and IRoutes from the same server slice (see BuildClientIPs / + // BuildIRoutes), so in practice every ClientIP has a matching IRoute. + iroute := irouteByName[clientIP.Name] + configurations = append(configurations, &remote.File{ + SSH: s.SSH, + Path: file.Path("/etc/openvpn/ccd/" + clientIP.Name.String()), + Content: content.Func(func(ctx context.Context) ([]byte, error) { + vpnIP, err := clientIP.IP.IP(ctx) + if err != nil { + return nil, err + } + serverNet, err := serverIPNet.IPNet(ctx) + if err != nil { + return nil, err + } + buf := &bytes.Buffer{} + fmt.Fprintf( + buf, + "ifconfig-push %s %s\n", + vpnIP.String(), + net.IP(serverNet.Mask).String(), + ) + if iroute != nil { + ipNet, err := iroute.IPNet(ctx) + if err != nil { + return nil, err + } + fmt.Fprintf( + buf, + "iroute %s %s\n", + ipNet.IP.String(), + net.IP(ipNet.Mask).String(), + ) + } + return buf.Bytes(), nil + }), + User: "root", + Group: "root", + Perm: 0600, + }) + } + return configurations, nil +} + +func (s *Server) serverConfig() ServerConfig { + return ServerConfig{ + ServerName: s.ServerName, + ServerIPNet: s.ServerIPNet, + ServerPort: network.PortStatic(563), + Routes: s.Routes, + Device: s.Device, + } +} + +func (s *Server) Applier() (world.Applier, error) { + return nil, nil +} + +type ipPool []ipPoolEntry + +func (i ipPool) Len() int { return len(i) } + +func (i ipPool) Less(a, b int) bool { + c := bytes.Compare(i[a].ip, i[b].ip) + if c < 0 { + return true + } + if c > 0 { + return false + } + return i[a].name < i[b].name +} + +func (i ipPool) Swap(a, b int) { i[a], i[b] = i[b], i[a] } + +func (i *ipPool) Bytes() []byte { + if i == nil { + return nil + } + buf := &bytes.Buffer{} + for _, e := range *i { + buf.Write(e.Bytes()) + } + return buf.Bytes() +} + +type ipPoolEntry struct { + name string + ip net.IP +} + +func (e ipPoolEntry) Bytes() []byte { + buf := &bytes.Buffer{} + fmt.Fprint(buf, e.name) + fmt.Fprint(buf, ",") + fmt.Fprintln(buf, e.ip.String()) + return buf.Bytes() +} + +func (s *Server) ipPoolContent() content.HasContent { + return content.Func(func(ctx context.Context) ([]byte, error) { + var result ipPool + for _, clientIP := range s.ClientIPs { + ip, err := clientIP.IP.IP(ctx) + if err != nil { + return nil, err + } + result = append(result, ipPoolEntry{ + name: clientIP.Name.String(), + ip: ip, + }) + } + sort.Sort(result) + return result.Bytes(), nil + }) +}