Skip to content

Commit

Permalink
Added certificate configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
ameshkov committed Feb 3, 2024
1 parent e8a14ab commit fa78666
Show file tree
Hide file tree
Showing 7 changed files with 306 additions and 59 deletions.
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,23 @@ adheres to [Semantic Versioning][semver].

## [Unreleased]

[unreleased]: https://github.com/ameshkov/udptlspipe/compare/v1.0.1...HEAD
[unreleased]: https://github.com/ameshkov/udptlspipe/compare/v1.1.0...HEAD

## [1.1.0] - 2024-02-03

* Added an option to configure custom TLS certificate. Check out
[README][readmetlscert] for more information on how to use that.

[1.1.0]: https://github.com/ameshkov/udptlspipe/releases/tag/v1.1.0

[readmetlscert]: https://github.com/ameshkov/udptlspipe?tab=readme-ov-file#tlscert

## [1.0.1] - 2024-02-02

* Added a [docker image][dockerregistry].

[dockerregistry]: https://github.com/ameshkov/udptlspipe/pkgs/container/udptlspipe

[1.0.1]: https://github.com/ameshkov/udptlspipe/releases/tag/v1.0.1

## [1.0.0] - 2024-02-02
Expand Down
79 changes: 74 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ to keep it that way.
* [Why would you need it?](#why)
* [How to install udptlspipe](#install)
* [How to use udptlspipe](#howtouse)
* [Custom TLS certificate](#tlscert)
* [Docker](#docker)
* [All command-line arguments](#allcmdarguments)

Expand Down Expand Up @@ -103,6 +104,58 @@ need to make some adjustments to the WireGuard client configuration:

[wireguardcalculator]: https://www.procustodibus.com/blog/2021/03/wireguard-allowedips-calculator/

<a id="tlscert"></a>

## Custom TLS certificate

By default, `udptlspipe` generates a self-signed certificate every time you run
a server, and the client does not verify the server certificate. This is an
okay-ish solution for a simple case when the authentication is handled by the
downstream UDP server, but it's not ideal when you want to completely secure
your tunnel. In order to achieve that goal, there is an option to use a custom
TLS certificate on the server-side and to enable certificates verification by
the client.
The first step would be to obtain a valid TLS certificate. You will probably
need to have a domain name to generate a valid TLS certificate. There are
numerous ways to do that, I suggest using a tool like [lego][lego] to automate
this process.
Here is how to run the server with a custom TLS certificate.
```shell
udptlspipe --server \
-l 0.0.0.0:443 \
-d 2.3.4.5:8123 \
-p SecurePassword \
--tls-servername yourdomain.com \
--tls-certfile /path/to/cert \
--tls-keyfile /path/to/key
```
* `--tls-servername` is the server name (should be the same as in your
certificate).
* `--tls-certfile` is a path to the file with your PEM-encoded certificate.
* `--tls-keyfile` is a path to the file with your PEM-encoded private key.
Now let's run the client so that it could verify the certificate:

```shell
udptlspipe \
-l 127.0.0.1:8123 \
-d 1.2.3.4:443 \
-p SecurePassword \
--secure \
--tls-servername yourdomain.com
```

* `--secure` enables TLS certificate verification.
* `--tls-servername` is the server name of the server cert.

[lego]: https://go-acme.github.io/lego/usage/cli/obtain-a-certificate/

<a id="docker"></a>

## Docker
Expand Down Expand Up @@ -143,12 +196,26 @@ Usage:
udptlspipe [OPTIONS]
Application Options:
-s, --server Enables the server mode. By default it runs in client mode.
-s, --server Enables the server mode (optional). By default it runs
in client mode.
-l, --listen=<IP:Port> Address the tool will be listening to (required).
-d, --destination=<IP:Port> Address the tool will connect to (required).
-p, --password=<password> Password is used to detect if the client is allowed.
-x, --proxy=[protocol://username:password@]host[:port] URL of a proxy to use when connecting to the destination address
-p, --password=<password> Password is used to detect if the client is allowed
(optional).
-x, --proxy=[protocol://username:password@]host[:port] URL of a proxy to use when connecting to the
destination address (optional).
--secure Enables server TLS certificate verification in client
mode (optional).
--tls-servername=<hostname> Configures TLS server name that will be sent in the TLS
ClientHello in client mode, and the stub certificate
name in server mode. If not set, the the default domain
name (example.org) will be used (optional).
--tls-certfile=<path-to-cert-file> Path to the TLS certificate file. Allows to use a
custom certificate in server mode. If not set, the
server will generate a self-signed stub certificate
(optional).
--tls-keyfile=<path-to-key-file> Path to the private key for the cert specified in
tls-certfile.
-v, --verbose Verbose output (optional).
Help Options:
Expand All @@ -157,5 +224,7 @@ Help Options:
## TODO
* [ ] Automatic TLS certs generation (let's encrypt, lego)
* [ ] Docker image
* [X] Docker image.
* [X] Certificate configuration.
* [ ] Use WebSocket for transport instead of the custom binary proto.
* [ ] Automatic TLS certs generation (let's encrypt, lego).
59 changes: 58 additions & 1 deletion internal/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"syscall"
"time"

tls "github.com/refraction-networking/utls"

"github.com/AdguardTeam/golibs/log"
"github.com/ameshkov/udptlspipe/internal/pipe"
"github.com/ameshkov/udptlspipe/internal/version"
Expand Down Expand Up @@ -42,7 +44,36 @@ func Main() {
log.SetLevel(log.DEBUG)
}

srv, err := pipe.NewServer(o.ListenAddr, o.DestinationAddr, o.Password, o.ProxyURL, o.ServerMode)
log.Info("Configuration:\n%s", o)

cfg := &pipe.Config{
ListenAddr: o.ListenAddr,
DestinationAddr: o.DestinationAddr,
Password: o.Password,
ServerMode: o.ServerMode,
ProxyURL: o.ProxyURL,
VerifyCertificate: o.VerifyCertificate,
TLSServerName: o.TLSServerName,
}

if o.TLSCertPath != "" {
if !o.ServerMode {
log.Error("TLS certificate only works in server mode")

os.Exit(1)
}

cert, certErr := loadX509KeyPair(o.TLSCertPath, o.TLSCertKey)
if certErr != nil {
log.Error("Failed to load TLS certificate: %v", err)

os.Exit(1)
}

cfg.TLSCertificate = cert
}

srv, err := pipe.NewServer(cfg)
if err != nil {
log.Error("Failed to initialize server: %v", err)

Expand Down Expand Up @@ -73,3 +104,29 @@ func Main() {

log.Info("Exiting udptlspipe.")
}

// loadX509KeyPair reads and parses a public/private key pair from a pair of
// files. The files must contain PEM encoded data. The certificate file may
// contain intermediate certificates following the leaf certificate to form a
// certificate chain. On successful return, Certificate.Leaf will be nil
// because the parsed form of the certificate is not retained.
func loadX509KeyPair(certFile, keyFile string) (crt *tls.Certificate, err error) {
// #nosec G304 -- Trust the file path that is given in the configuration.
certPEMBlock, err := os.ReadFile(certFile)
if err != nil {
return nil, err
}

// #nosec G304 -- Trust the file path that is given in the configuration.
keyPEMBlock, err := os.ReadFile(keyFile)
if err != nil {
return nil, err
}

tlsCert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
if err != nil {
return nil, err
}

return &tlsCert, nil
}
47 changes: 41 additions & 6 deletions internal/cmd/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,71 @@ import (
"fmt"
"os"

"gopkg.in/yaml.v3"

goFlags "github.com/jessevdk/go-flags"
)

// Options represents command-line arguments.
type Options struct {
// ServerMode controls whether the tool works in the server mode.
// By default, the tool will work in the client mode.
ServerMode bool `short:"s" long:"server" description:"Enables the server mode. By default it runs in client mode." optional:"yes" optional-value:"true"`
ServerMode bool `yaml:"server" short:"s" long:"server" description:"Enables the server mode (optional). By default it runs in client mode." optional:"yes" optional-value:"true"`

// ListenAddr is the address the tool will be listening to. If it's in the
// pipe mode, it will listen to tcp://, if it's in the client mode, it
// will listen to udp://.
ListenAddr string `short:"l" long:"listen" description:"Address the tool will be listening to (required)." value-name:"<IP:Port>" required:"true"`
ListenAddr string `yaml:"listen" short:"l" long:"listen" description:"Address the tool will be listening to (required)." value-name:"<IP:Port>" required:"true"`

// DestinationAddr is the address the tool will connect to. Depending on the
// mode (pipe or client) this address has different semantics. In the
// client mode this is the address of the udptlspipe pipe. In the pipe
// mode this is the address where the received traffic will be passed.
DestinationAddr string `short:"d" long:"destination" description:"Address the tool will connect to (required)." value-name:"<IP:Port>" required:"true"`
DestinationAddr string `yaml:"destination" short:"d" long:"destination" description:"Address the tool will connect to (required)." value-name:"<IP:Port>" required:"true"`

// Password is used to detect if the client is actually allowed to use
// udptlspipe. If it's not allowed, the server returns a stub web page.
Password string `short:"p" long:"password" description:"Password is used to detect if the client is allowed." value-name:"<password>"`
Password string `yaml:"password" short:"p" long:"password" description:"Password is used to detect if the client is allowed (optional)." value-name:"<password>"`

// ProxyURL is the proxy address that should be used when connecting to the
// destination address.
ProxyURL string `short:"x" long:"proxy" description:"URL of a proxy to use when connecting to the destination address (optional)." value-name:"[protocol://username:password@]host[:port]"`
ProxyURL string `yaml:"proxy" short:"x" long:"proxy" description:"URL of a proxy to use when connecting to the destination address (optional)." value-name:"[protocol://username:password@]host[:port]"`

// VerifyCertificate enables server certificate verification in client mode.
// If enabled, the client will verify the server certificate using the
// system root certs store.
VerifyCertificate bool `yaml:"secure" long:"secure" description:"Enables server TLS certificate verification in client mode (optional)." optional:"yes" optional-value:"true"`

// TLSServerName configures the server name to send in TLS ClientHello when
// operating in client mode and the server name that will be used when
// generating a stub certificate. If not set, the default domain name will
// be used for these purposes.
TLSServerName string `yaml:"tls-servername" long:"tls-servername" description:"Configures TLS server name that will be sent in the TLS ClientHello in client mode, and the stub certificate name in server mode. If not set, the the default domain name (example.org) will be used (optional)." value-name:"<hostname>"`

// TLSCertPath is a path to the TLS certificate file. Allows to use a custom
// certificate in server mode. If not set, the server will generate a
// self-signed stub certificate.
TLSCertPath string `yaml:"tls-certfile" long:"tls-certfile" description:"Path to the TLS certificate file. Allows to use a custom certificate in server mode. If not set, the server will generate a self-signed stub certificate (optional)." value-name:"<path-to-cert-file>"`

// TLSCertKey is a path to the file with the private key to the TLS
// certificate specified by TLSCertPath.
TLSCertKey string `yaml:"tls-keyfile" long:"tls-keyfile" description:"Path to the private key for the cert specified in tls-certfile." value-name:"<path-to-key-file>"`

// Verbose defines whether we should write the DEBUG-level log or not.
Verbose bool `short:"v" long:"verbose" description:"Verbose output (optional)." optional:"yes" optional-value:"true"`
Verbose bool `yaml:"verbose" short:"v" long:"verbose" description:"Verbose output (optional)." optional:"yes" optional-value:"true"`
}

// type check
var _ fmt.Stringer = (*Options)(nil)

// String implements the fmt.Stringer interface for *Options.
func (o *Options) String() (str string) {
b, err := yaml.Marshal(o)
if err != nil {
return fmt.Sprintf("Failed to stringify options due to %s", err)
}

return string(b)
}

// parseOptions parses os.Args and creates the Options struct.
Expand Down
Loading

0 comments on commit fa78666

Please sign in to comment.