Compare commits

..

9 Commits

Author SHA1 Message Date
Simon Ser 60cab19e46 Don't print nil connection errors 2023-02-20 14:40:44 +01:00
delthas d314adee59 Add support for backend PROXY protocol v1
This is enabled with backend /* ... */ { proxy_version 1 }
2023-02-09 15:28:44 +01:00
Simon Ser 84ae2e62d6 Show more errors without -debug
Some errors should be surfaced back even without -debug: for
instance, failure to connect to the backend.
2023-01-27 11:04:36 +01:00
Simon Ser 151e7cf586 Add support for certificate fingerprint pinning 2023-01-27 10:55:53 +01:00
Simon Ser ce4e23e5d8 man: only one URI can be supplied to the backend directive
Multiple URIs is something worth supporting, but we're not there
yet.
2023-01-27 10:39:52 +01:00
Simon Ser 86308c9780 Fix ACME DNS challenge for top-level domains in a zone
e.g. "*.emersion.fr" when the zone is "emersion.fr".

Fixes: 662136ea74 ("Add support for ACME DNS hooks")
2023-01-26 19:14:08 +01:00
Simon Ser 662136ea74 Add support for ACME DNS hooks
Closes: https://todo.sr.ht/~emersion/tlstunnel/2
2023-01-26 17:04:45 +01:00
Simon Ser 3fd3471799 Silence connection errors by default
Often times the connection-level errors clutter the logs, for
instance with failed TLS handshakes or unknown hostname.
2023-01-26 11:43:59 +01:00
Simon Ser bb3c49e3b5 readme: restrict CI badge to master branch 2023-01-12 19:29:33 +01:00
16 changed files with 211 additions and 147 deletions

View File

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

View File

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

11
debian/changelog vendored
View File

@ -1,11 +0,0 @@
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
View File

@ -1,37 +0,0 @@
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
View File

@ -1,31 +0,0 @@
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
View File

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

40
debian/postinst vendored
View File

@ -1,40 +0,0 @@
#!/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
View File

@ -1,13 +0,0 @@
#!/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)"

View File

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

View File

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

View File

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

View File

@ -1,12 +1,17 @@
package tlstunnel
import (
"crypto/sha256"
"crypto/subtle"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"fmt"
"net"
"net/url"
"os"
"os/exec"
"strconv"
"strings"
"git.sr.ht/~emersion/go-scfg"
@ -130,6 +135,64 @@ func parseBackend(backend *Backend, d *scfg.Directive) error {
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
}
@ -175,6 +238,19 @@ func parseTLS(srv *Server, d *scfg.Directive) error {
if err := parseTLSOnDemand(srv, child); err != nil {
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:
return fmt.Errorf("unknown %q directive", child.Name)
}

81
dns.go Normal file
View File

@ -0,0 +1,81 @@
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,6 +6,7 @@ require (
git.sr.ht/~emersion/go-scfg v0.0.0-20211215104734-c2c7a15d6c99
github.com/caddyserver/certmagic v0.17.2
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/pkg/errors v0.9.1 // indirect
go.uber.org/atomic v1.10.0 // indirect

View File

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

View File

@ -52,7 +52,7 @@ The following directives are supported:
*listen* <address>...
Additional addresses to listen on.
*backend* <uri>...
*backend* <uri> { ... }
Backend to forward incoming connections to.
The following URIs are supported:
@ -64,6 +64,25 @@ The following directives are supported:
The _+proxy_ suffix can be added to the URI scheme to forward
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* { ... }
Customise frontend-specific TLS configuration.
@ -123,6 +142,13 @@ The following directives are supported:
The environment will contain a *TLSTUNNEL_NAME* variable with the
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
_/etc/tlstunnel/config_