Skip to content

Commit

Permalink
wireguard: Initial commit of matcher (#245)
Browse files Browse the repository at this point in the history
  • Loading branch information
vnxme authored Sep 12, 2024
1 parent 3d22d6d commit 4f012d4
Show file tree
Hide file tree
Showing 5 changed files with 484 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Current matchers:
- **layer4.matchers.socks5** - matches connections that look like [SOCKSv5](https://www.rfc-editor.org/rfc/rfc1928.html).
- **layer4.matchers.ssh** - matches connections that look like SSH connections.
- **layer4.matchers.tls** - matches connections that start with TLS handshakes. In addition, any [`tls.handshake_match` modules](https://caddyserver.com/docs/modules/) can be used for matching on TLS-specific properties of the ClientHello, such as ServerName (SNI).
- **layer4.matchers.wireguard** - matches connections the look like [WireGuard](https://www.wireguard.com/protocol/) connections.
- **layer4.matchers.xmpp** - matches connections that look like [XMPP](https://xmpp.org/about/technology-overview/).

Current handlers:
Expand Down
1 change: 1 addition & 0 deletions imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ import (
_ "github.com/mholt/caddy-l4/modules/l4tee"
_ "github.com/mholt/caddy-l4/modules/l4throttle"
_ "github.com/mholt/caddy-l4/modules/l4tls"
_ "github.com/mholt/caddy-l4/modules/l4wireguard"
_ "github.com/mholt/caddy-l4/modules/l4xmpp"
)
80 changes: 80 additions & 0 deletions integration/caddyfile_adapt/gd_matcher_wireguard.caddytest
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{
layer4 {
udp/:51820 {
@wg0 wireguard
route @wg0 {
proxy udp/wg.machine.local:51820
}
@wgX wireguard 4285988864
route @wgX {
proxy udp/wg.machine.local:51821
}
route {
echo
}
}
}
}
----------
{
"apps": {
"layer4": {
"servers": {
"srv0": {
"listen": [
"udp/:51820"
],
"routes": [
{
"match": [
{
"wireguard": {}
}
],
"handle": [
{
"handler": "proxy",
"upstreams": [
{
"dial": [
"udp/wg.machine.local:51820"
]
}
]
}
]
},
{
"match": [
{
"wireguard": {
"zero": 4285988864
}
}
],
"handle": [
{
"handler": "proxy",
"upstreams": [
{
"dial": [
"udp/wg.machine.local:51821"
]
}
]
}
]
},
{
"handle": [
{
"handler": "echo"
}
]
}
]
}
}
}
}
}
258 changes: 258 additions & 0 deletions modules/l4wireguard/matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
// Copyright 2024 VNXME
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package l4wireguard

import (
"bytes"
"encoding/binary"
"io"
"strconv"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"

"github.com/mholt/caddy-l4/layer4"
)

func init() {
caddy.RegisterModule(&MatchWireGuard{})
}

// MatchWireGuard is able to match WireGuard connections.
type MatchWireGuard struct {
// Zero may be used to match reserved zero bytes of Type field when
// they have non-zero values (e.g. for obfuscation purposes). E.g. it
// may be set to 4,285,988,864 (0xFF770000) in order to match custom
// handshake initiation messages starting with 0x010077FF byte sequence.
// Note: any non-zero value is a violation of the WireGuard protocol.
Zero uint32 `json:"zero,omitempty"`
}

// CaddyModule returns the Caddy module information.
func (m *MatchWireGuard) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "layer4.matchers.wireguard",
New: func() caddy.Module { return new(MatchWireGuard) },
}
}

// Match returns true if the connection looks like WireGuard.
func (m *MatchWireGuard) Match(cx *layer4.Connection) (bool, error) {
// Read a number of bytes
buf := make([]byte, MessageInitiationBytesTotal+1)
n, err := io.ReadAtLeast(cx, buf, 1)
if err != nil {
return false, err
}

switch n {
case MessageInitiationBytesTotal: // This is a handshake initiation message
// Parse MessageInitiation
msg := &MessageInitiation{}
if err = msg.FromBytes(buf[:MessageInitiationBytesTotal]); err != nil {
return false, nil
}

// Validate MessageInitiation
if msg.Type != (m.Zero&ReservedZeroFilter)|MessageTypeInitiation {
return false, nil
}
case MessageTransportBytesMin: // This is a keepalive message (with empty content)
// Parse MessageTransport
msg := &MessageTransport{}
if err = msg.FromBytes(buf[:MessageTransportBytesMin]); err != nil {
return false, nil
}

// Validate MessageTransport
if msg.Type != (m.Zero&ReservedZeroFilter)|MessageTypeTransport {
return false, nil
}
default: // This is anything else, can also be a valid non-empty transport message
return false, nil
}

return true, nil
}

// Provision prepares m's internal structures.
func (m *MatchWireGuard) Provision(_ caddy.Context) error {
return nil
}

// UnmarshalCaddyfile sets up the MatchWireGuard from Caddyfile tokens. Syntax:
//
// wireguard [<zero>]
func (m *MatchWireGuard) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
_, wrapper := d.Next(), d.Val() // consume wrapper name

// Only one same-line argument is supported
if d.CountRemainingArgs() > 1 {
return d.ArgErr()
}

if d.NextArg() {
val, err := strconv.ParseUint(d.Val(), 10, 32)
if err != nil {
return d.Errf("parsing %s zero: %v", wrapper, err)
}
m.Zero = uint32(val)
}

// No blocks are supported
if d.NextBlock(d.Nesting()) {
return d.Errf("malformed %s option: blocks are not supported", wrapper)
}

return nil

}

// MessageInitiation is the first message
// which the initiator sends to the responder.
type MessageInitiation struct {
Type uint32
Sender uint32
Ephemeral [32]uint8
Static [32 + Poly1305TagSize]uint8
Timestamp [12 + Poly1305TagSize]uint8
MAC1 [16]uint8
MAC2 [16]uint8
}

func (msg *MessageInitiation) FromBytes(src []byte) error {
buf := bytes.NewBuffer(src)
if err := binary.Read(buf, MessageBytesOrder, &msg.Type); err != nil {
return err
}
if err := binary.Read(buf, MessageBytesOrder, &msg.Sender); err != nil {
return err
}
if err := binary.Read(buf, MessageBytesOrder, &msg.Ephemeral); err != nil {
return err
}
if err := binary.Read(buf, MessageBytesOrder, &msg.Static); err != nil {
return err
}
if err := binary.Read(buf, MessageBytesOrder, &msg.Timestamp); err != nil {
return err
}
if err := binary.Read(buf, MessageBytesOrder, &msg.MAC1); err != nil {
return err
}
if err := binary.Read(buf, MessageBytesOrder, &msg.MAC2); err != nil {
return err
}
return nil
}

func (msg *MessageInitiation) ToBytes() ([]byte, error) {
dst := bytes.NewBuffer(make([]byte, 0, MessageInitiationBytesTotal))
if err := binary.Write(dst, MessageBytesOrder, &msg.Type); err != nil {
return nil, err
}
if err := binary.Write(dst, MessageBytesOrder, &msg.Sender); err != nil {
return nil, err
}
if err := binary.Write(dst, MessageBytesOrder, &msg.Ephemeral); err != nil {
return nil, err
}
if err := binary.Write(dst, MessageBytesOrder, &msg.Static); err != nil {
return nil, err
}
if err := binary.Write(dst, MessageBytesOrder, &msg.Timestamp); err != nil {
return nil, err
}
if err := binary.Write(dst, MessageBytesOrder, &msg.MAC1); err != nil {
return nil, err
}
if err := binary.Write(dst, MessageBytesOrder, &msg.MAC2); err != nil {
return nil, err
}
return dst.Bytes(), nil
}

// MessageTransport is the message which the initiator and
// the responder exchange after a successful handshake.
type MessageTransport struct {
Type uint32
Receiver uint32
Counter uint64
Content []uint8
}

func (msg *MessageTransport) FromBytes(src []byte) error {
buf := bytes.NewBuffer(src)
if err := binary.Read(buf, MessageBytesOrder, &msg.Type); err != nil {
return err
}
if err := binary.Read(buf, MessageBytesOrder, &msg.Receiver); err != nil {
return err
}
if err := binary.Read(buf, MessageBytesOrder, &msg.Counter); err != nil {
return err
}
if buf.Len() > 0 {
msg.Content = append(msg.Content, buf.Bytes()...)
}
return nil
}

func (msg *MessageTransport) ToBytes() ([]byte, error) {
dst := bytes.NewBuffer(make([]byte, 0, MessageTransportBytesMin-Poly1305TagSize+len(msg.Content)))
if err := binary.Write(dst, MessageBytesOrder, &msg.Type); err != nil {
return nil, err
}
if err := binary.Write(dst, MessageBytesOrder, &msg.Receiver); err != nil {
return nil, err
}
if err := binary.Write(dst, MessageBytesOrder, &msg.Counter); err != nil {
return nil, err
}
return append(dst.Bytes(), msg.Content...), nil
}

// Interface guards
var (
_ caddy.Provisioner = (*MatchWireGuard)(nil)
_ caddyfile.Unmarshaler = (*MatchWireGuard)(nil)
_ layer4.ConnMatcher = (*MatchWireGuard)(nil)
)

var (
MessageBytesOrder = binary.LittleEndian
)

// Refs:
//
// https://www.wireguard.com/protocol/
// https://www.wireguard.com/papers/wireguard.pdf
// https://github.com/pirate/wireguard-docs
// https://github.com/WireGuard/wireguard-go/blob/master/device/noise-protocol.go
const (
Poly1305TagSize int = 16

MessageInitiationBytesTotal int = 148
MessageResponseBytesTotal int = 92
MessageCookieReplyBytesTotal int = 64
MessageTransportBytesMin int = 32

MessageTypeInitiation uint32 = 1
MessageTypeResponse uint32 = 2
MessageTypeCookieReply uint32 = 3
MessageTypeTransport uint32 = 4

ReservedZeroFilter = ^(uint32(0)) >> 8 << 8
)
Loading

0 comments on commit 4f012d4

Please sign in to comment.