Compare commits

..

2 Commits

16 changed files with 147 additions and 211 deletions

View File

@ -1,6 +1,6 @@
# [tlstunnel] # [tlstunnel]
[![builds.sr.ht status](https://builds.sr.ht/~emersion/tlstunnel/commits/master.svg)](https://builds.sr.ht/~emersion/tlstunnel/commits/master?) [![builds.sr.ht status](https://builds.sr.ht/~emersion/tlstunnel/commits.svg)](https://builds.sr.ht/~emersion/tlstunnel/commits?)
A TLS reverse proxy. A TLS reverse proxy.

View File

@ -17,8 +17,6 @@ import (
var ( var (
configPath = "config" configPath = "config"
certDataPath = "" certDataPath = ""
debug = false
) )
func newServer() (*tlstunnel.Server, error) { func newServer() (*tlstunnel.Server, error) {
@ -28,7 +26,6 @@ func newServer() (*tlstunnel.Server, error) {
} }
srv := tlstunnel.NewServer() srv := tlstunnel.NewServer()
srv.Debug = debug
loggerCfg := zap.Config{ loggerCfg := zap.Config{
Level: zap.NewAtomicLevelAt(zap.InfoLevel), Level: zap.NewAtomicLevelAt(zap.InfoLevel),
@ -71,7 +68,6 @@ func bumpOpenedFileLimit() error {
func main() { func main() {
flag.StringVar(&configPath, "config", configPath, "path to configuration file") flag.StringVar(&configPath, "config", configPath, "path to configuration file")
flag.BoolVar(&debug, "debug", false, "enable debug logging")
flag.Parse() flag.Parse()
if err := bumpOpenedFileLimit(); err != nil { if err := bumpOpenedFileLimit(); err != nil {

11
debian/changelog vendored Normal file
View File

@ -0,0 +1,11 @@
tlstunnel (0.2.0-2) bookworm; urgency=medium
* Set default config and certificates path with linker flags
-- jordi fita mas <jordi@tandem.blog> Thu, 15 Jun 2023 13:19:45 +0000
tlstunnel (0.2.0-1) bullseye; urgency=medium
* Add Debian packaging
-- jordi fita mas <jordi@tandem.blog> Wed, 14 Jun 2023 20:40:39 +0000

37
debian/control vendored Normal file
View File

@ -0,0 +1,37 @@
Source: tlstunnel
Section: net
Priority: optional
Maintainer: jordi fita mas <jordi@tandem.blog>
Build-Depends:
debhelper-compat (= 13),
dh-golang,
golang-any,
golang-sourcehut-emersion-go-scfg-dev,
golang-github-caddyserver-certmagic-dev,
golang-github-klauspost-cpuid-dev,
golang-github-libdns-libdns-dev,
golang-github-pires-go-proxyproto-dev,
golang-github-pkg-errors-dev,
golang-go.uber-atomic-dev,
golang-go.uber-multierr-dev,
golang-go.uber-zap-dev,
golang-golang-x-crypto-dev,
golang-golang-x-tools-dev,
scdoc
Standards-Version: 4.6.0
Vcs-Browser: https://git.sr.ht/~emersion/tlstunnel/tree
Vcs-Git: https://git.sr.ht/~emersion/tlstunnel
Homepage: https://git.sr.ht/~emersion/tlstunnel
Rules-Requires-Root: no
XS-Go-Import-Path: git.sr.ht/~emersion/tlstunnel
Package: tlstunnel
Architecture: any
Depends:
${shlibs:Depends},
${misc:Depends}
Built-Using: ${misc:Built-Using}
description: TLS reverse proxy
.
It features: Automatic TLS with Let's Encrypt, routing incoming connections to
backends using Server Name Indication, and support for the PROXY protocol.

31
debian/copyright vendored Normal file
View File

@ -0,0 +1,31 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Source: https://git.sr.ht/~emersion/tlstunnel
Upstream-Name: tlstunnel
Upstream-Contact: ~emersion/public-inbox@lists.sr.ht
Files: *
Copyright: 20202023 Simon Ser
License: MIT
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Files: debian/*
Copyright: 2023 jordi fita mas
License: MIT
This debian package is distributed under the same license as the source
package.

1
debian/manpages vendored Normal file
View File

@ -0,0 +1 @@
tlstunnel.1

40
debian/postinst vendored Normal file
View File

@ -0,0 +1,40 @@
#!/bin/sh
set -e
. /usr/share/debconf/confmodule
case "$1" in
configure)
# Create tlstunnel user and group
if ! getent group tlstunnel >/dev/null; then
addgroup --system --quiet tlstunnel
fi
if ! getent passwd tlstunnel >/dev/null; then
adduser --quiet \
--system \
--disabled-login \
--no-create-home \
--shell /bin/bash \
--ingroup tlstunnel \
--home /var/lib/tlstunnel \
--gecos "tlstunnel daemon" \
tlstunnel
fi
# Make sure lib directory has correct permissions set
dpkg-statoverride --list "/var/lib/tlstunnel" >/dev/null || \
dpkg-statoverride --add --force --quiet --update tlstunnel adm 0750 /var/lib/tlstunnel
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
#DEBHELPER#
exit 0

13
debian/rules vendored Executable file
View File

@ -0,0 +1,13 @@
#!/usr/bin/make -f
GO_LDFLAGS += -X 'main.configPath=/etc/tlstunnel'
GO_LDFLAGS += -X 'main.certDataPath=/var/lib/tlstunnel'
%:
dh $@ --builddirectory=_build --buildsystem=golang --with=golang
execute_before_dh_auto_build:
make tlstunnel.1
override_dh_auto_build:
dh_auto_build -- -ldflags "$(GO_LDFLAGS)"

1
debian/source/format vendored Normal file
View File

@ -0,0 +1 @@
3.0 (quilt)

1
debian/tlstunnel.service vendored Symbolic link
View File

@ -0,0 +1 @@
../contrib/systemd/tlstunnel.service

1
debian/tlstunnel.tmpfiles vendored Symbolic link
View File

@ -0,0 +1 @@
../contrib/systemd/tlstunnel.tmpfiles

View File

@ -1,17 +1,12 @@
package tlstunnel package tlstunnel
import ( import (
"crypto/sha256"
"crypto/subtle"
"crypto/tls" "crypto/tls"
"crypto/x509"
"encoding/hex"
"fmt" "fmt"
"net" "net"
"net/url" "net/url"
"os" "os"
"os/exec" "os/exec"
"strconv"
"strings" "strings"
"git.sr.ht/~emersion/go-scfg" "git.sr.ht/~emersion/go-scfg"
@ -135,64 +130,6 @@ func parseBackend(backend *Backend, d *scfg.Directive) error {
return fmt.Errorf("failed to setup backend %q: unsupported URI scheme", backendURI) return fmt.Errorf("failed to setup backend %q: unsupported URI scheme", backendURI)
} }
for _, child := range d.Children {
switch child.Name {
case "tls_certfp":
if backend.TLSConfig == nil {
return fmt.Errorf("tls_certfp requires a tls:// backend address")
}
var algo, wantCertFP string
if err := child.ParseParams(&algo, &wantCertFP); err != nil {
return err
}
if algo != "sha-256" {
return fmt.Errorf("directive tls_certfp: only sha-256 is supported")
}
wantCertFP = strings.ReplaceAll(wantCertFP, ":", "")
wantSum, err := hex.DecodeString(wantCertFP)
if err != nil {
return fmt.Errorf("directive tls_certfp: invalid fingerprint: %v", err)
} else if len(wantSum) != sha256.Size {
return fmt.Errorf("directive tls_certfp: invalid fingerprint length")
}
backend.TLSConfig.InsecureSkipVerify = true
backend.TLSConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
if len(rawCerts) == 0 {
return fmt.Errorf("the server didn't present any TLS certificate")
}
for _, rawCert := range rawCerts {
sum := sha256.Sum256(rawCert)
if subtle.ConstantTimeCompare(sum[:], wantSum) == 1 {
return nil // fingerprints match
}
}
sum := sha256.Sum256(rawCerts[0])
remoteCertFP := hex.EncodeToString(sum[:])
return fmt.Errorf("configured TLS certificate fingerprint doesn't match the server's - %s", remoteCertFP)
}
case "proxy_version":
var version string
if err := child.ParseParams(&version); err != nil {
return err
}
v, err := strconv.Atoi(version)
if err != nil {
return fmt.Errorf("directive proxy_version: invalid version: %v", err)
}
switch v {
case 1, 2:
backend.ProxyVersion = v
default:
return fmt.Errorf("directive proxy_version: unknown version: %v", v)
}
}
}
return nil return nil
} }
@ -238,19 +175,6 @@ func parseTLS(srv *Server, d *scfg.Directive) error {
if err := parseTLSOnDemand(srv, child); err != nil { if err := parseTLSOnDemand(srv, child); err != nil {
return err return err
} }
case "acme_dns_command":
var cmdName string
if err := child.ParseParams(&cmdName); err != nil {
return err
}
cmdParams := child.Params[1:]
srv.ACMEIssuer.DNS01Solver = &certmagic.DNS01Solver{
DNSProvider: &commandDNSProvider{
Name: cmdName,
Params: cmdParams,
},
}
default: default:
return fmt.Errorf("unknown %q directive", child.Name) return fmt.Errorf("unknown %q directive", child.Name)
} }

81
dns.go
View File

@ -1,81 +0,0 @@
package tlstunnel
import (
"context"
"fmt"
"os/exec"
"strings"
"github.com/caddyserver/certmagic"
"github.com/libdns/libdns"
)
type commandDNSProvider struct {
Name string
Params []string
}
var _ certmagic.ACMEDNSProvider = (*commandDNSProvider)(nil)
func (provider *commandDNSProvider) exec(ctx context.Context, subcmd string, subargs ...string) error {
var params []string
params = append(params, provider.Params...)
params = append(params, subcmd)
params = append(params, subargs...)
cmd := exec.CommandContext(ctx, provider.Name, params...)
if out, err := cmd.CombinedOutput(); err != nil {
details := ""
if len(out) > 0 {
details = ": " + string(out)
}
return fmt.Errorf("failed to run DNS hook %v (%w)%v", subcmd, err, details)
}
return nil
}
func (provider *commandDNSProvider) processRecords(ctx context.Context, zone string, recs []libdns.Record, subcmd string) ([]libdns.Record, error) {
var (
done []libdns.Record
err error
)
for _, rec := range recs {
var domain string
if domain, err = domainFromACMEChallengeRecord(zone, &rec); err != nil {
break
}
if err = provider.exec(ctx, subcmd, domain, "-", rec.Value); err != nil {
break
}
done = append(done, rec)
}
return done, err
}
func (provider *commandDNSProvider) AppendRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
return provider.processRecords(ctx, zone, recs, "deploy_challenge")
}
func (provider *commandDNSProvider) DeleteRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
return provider.processRecords(ctx, zone, recs, "clean_challenge")
}
func domainFromACMEChallengeRecord(zone string, rec *libdns.Record) (string, error) {
relZone := strings.TrimSuffix(zone, ".")
var domain string
if rec.Name == "_acme-challenge" {
// Root domain
domain = relZone
} else if strings.HasPrefix(rec.Name, "_acme-challenge.") {
// Subdomain
relName := strings.TrimPrefix(rec.Name, "_acme-challenge.")
domain = relName + "." + relZone
}
if rec.Type != "TXT" || domain == "" {
return "", fmt.Errorf("DNS record doesn't look like an ACME challenge: %v %v", rec.Type, rec.Name)
}
return domain, nil
}

1
go.mod
View File

@ -6,7 +6,6 @@ require (
git.sr.ht/~emersion/go-scfg v0.0.0-20211215104734-c2c7a15d6c99 git.sr.ht/~emersion/go-scfg v0.0.0-20211215104734-c2c7a15d6c99
github.com/caddyserver/certmagic v0.17.2 github.com/caddyserver/certmagic v0.17.2
github.com/klauspost/cpuid/v2 v2.2.1 // indirect github.com/klauspost/cpuid/v2 v2.2.1 // indirect
github.com/libdns/libdns v0.2.1
github.com/pires/go-proxyproto v0.6.2 github.com/pires/go-proxyproto v0.6.2
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
go.uber.org/atomic v1.10.0 // indirect go.uber.org/atomic v1.10.0 // indirect

View File

@ -38,7 +38,6 @@ func newACMECache() *acmeCache {
type Server struct { type Server struct {
Listeners map[string]*Listener // indexed by listening address Listeners map[string]*Listener // indexed by listening address
Frontends []*Frontend Frontends []*Frontend
Debug bool
ManagedNames []string ManagedNames []string
UnmanagedCerts []tls.Certificate UnmanagedCerts []tls.Certificate
@ -187,10 +186,6 @@ func (srv *Server) Replace(old *Server) error {
return nil return nil
} }
type clientError struct {
error
}
type listenerHandles struct { type listenerHandles struct {
Server *Server Server *Server
Frontends map[string]*Frontend // indexed by server name Frontends map[string]*Frontend // indexed by server name
@ -261,14 +256,8 @@ func (ln *Listener) serve() error {
} }
go func() { go func() {
err := ln.handle(conn) if err := ln.handle(conn); err != nil {
if err == nil { log.Printf("listener %q: %v", ln.Address, err)
return
}
srv := ln.atomic.Load().(*listenerHandles).Server
var clientErr clientError
if !errors.As(err, &clientErr) || srv.Debug {
log.Printf("listener %q: connection %q: %v", ln.Address, conn.RemoteAddr(), err)
} }
}() }()
} }
@ -309,7 +298,7 @@ func (ln *Listener) handle(conn net.Conn) error {
if err := tlsConn.Handshake(); err == io.EOF { if err := tlsConn.Handshake(); err == io.EOF {
return nil return nil
} else if err != nil { } else if err != nil {
return clientError{fmt.Errorf("TLS handshake failed: %v", err)} return fmt.Errorf("TLS handshake failed: %v", err)
} }
if err := tlsConn.SetDeadline(time.Time{}); err != nil { if err := tlsConn.SetDeadline(time.Time{}); err != nil {
return fmt.Errorf("failed to reset TLS handshake timeout: %v", err) return fmt.Errorf("failed to reset TLS handshake timeout: %v", err)
@ -369,7 +358,7 @@ func (fe *Frontend) handle(downstream net.Conn, tlsState *tls.ConnectionState) e
defer upstream.Close() defer upstream.Close()
if be.Proxy { if be.Proxy {
h := proxyproto.HeaderProxyFromAddrs(byte(be.ProxyVersion), downstream.RemoteAddr(), downstream.LocalAddr()) h := proxyproto.HeaderProxyFromAddrs(2, downstream.RemoteAddr(), downstream.LocalAddr())
var tlvs []proxyproto.TLV var tlvs []proxyproto.TLV
if tlsState.ServerName != "" { if tlsState.ServerName != "" {
@ -393,17 +382,16 @@ func (fe *Frontend) handle(downstream net.Conn, tlsState *tls.ConnectionState) e
} }
if err := duplexCopy(upstream, downstream); err != nil { if err := duplexCopy(upstream, downstream); err != nil {
return clientError{fmt.Errorf("failed to copy bytes: %v", err)} return fmt.Errorf("failed to copy bytes: %v", err)
} }
return nil return nil
} }
type Backend struct { type Backend struct {
Network string Network string
Address string Address string
Proxy bool Proxy bool
ProxyVersion int TLSConfig *tls.Config // nil if no TLS
TLSConfig *tls.Config // nil if no TLS
} }
func duplexCopy(a, b io.ReadWriter) error { func duplexCopy(a, b io.ReadWriter) error {

View File

@ -52,7 +52,7 @@ The following directives are supported:
*listen* <address>... *listen* <address>...
Additional addresses to listen on. Additional addresses to listen on.
*backend* <uri> { ... } *backend* <uri>...
Backend to forward incoming connections to. Backend to forward incoming connections to.
The following URIs are supported: The following URIs are supported:
@ -64,25 +64,6 @@ The following directives are supported:
The _+proxy_ suffix can be added to the URI scheme to forward The _+proxy_ suffix can be added to the URI scheme to forward
connection metadata via the PROXY protocol. connection metadata via the PROXY protocol.
The backend directive supports the following sub-directives:
*tls_certfp* sha-256 <fingerprint>
Instead of using CAs to check the TLS certificate provided by the
backend, check that the certificate matches the provided
fingerprint. This can be used to connect to servers with a
self-signed certificate, for instance.
The fingerprint of a certificate can be obtained via *openssl*(1):
```
openssl x509 -fingerprint -sha256 -noout <certificate>
```
*proxy_version* <version>
PROXY protocol version to use, if _+proxy_ is specified.
The supported versions are 1 and 2.
If not specified, the PROXY version used defaults to version 2.
*tls* { ... } *tls* { ... }
Customise frontend-specific TLS configuration. Customise frontend-specific TLS configuration.
@ -142,13 +123,6 @@ The following directives are supported:
The environment will contain a *TLSTUNNEL_NAME* variable with the The environment will contain a *TLSTUNNEL_NAME* variable with the
domain name to be validated. domain name to be validated.
*acme_dns_command* command [arguments...]
Configure the ACME DNS challenge using the specified command.
The command must implement _deploy_challenge_ and _clean_challenge_
as specified by dehydrated's hooks:
https://github.com/dehydrated-io/dehydrated
# FILES # FILES
_/etc/tlstunnel/config_ _/etc/tlstunnel/config_