initial commit

Signed-off-by: Steffen Vogel <post@steffenvogel.de>
This commit is contained in:
Steffen Vogel
2021-08-01 15:28:25 +02:00
commit a74df99adb
78 changed files with 8491 additions and 0 deletions

76
.github/workflows/build.yaml vendored Normal file
View File

@@ -0,0 +1,76 @@
on:
- push
- pull_request
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# The "build" workflow
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
# Setup Go
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: '1.16.6'
# Install all the dependencies
- name: Install dependencies
run: go get -u golang.org/x/lint/golint
# Run build of the application
- name: Run build
run: make
# Run vet on the code
- name: Run vet
run: go vet ./wice
# Run lint on the code
- name: Run lint
run: golint ./wice
# Run testing on the code
- name: Run testing
run: go test -v ./wice
# Run static check
- uses: reviewdog/action-staticcheck@v1
with:
github_token: ${{ secrets.github_token }}
reporter: github-pr-review
filter_mode: nofilter
fail_on_error: true
# The "deploy" workflow
deploy:
# The type of runner that the job will run on
runs-on: ubuntu-latest
needs: [build] # Only run this workflow when "build" workflow succeeds
if: ${{ github.ref == 'refs/heads/master' && github.event_name == 'push' }} # Only run this workflow if it is master branch on push event
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
tags: user/app:latest

23
.github/workflows/release.yaml vendored Normal file
View File

@@ -0,0 +1,23 @@
on:
release:
types:
- created
jobs:
linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.16.6
- name: Make Directory with kgctl Binaries to Be Released
run: make release
- name: Publish Release
uses: skx/github-action-publish-binaries@master
env:
GITHUB_TOKEN: ${{ secrets.github_token }}
with:
args: 'build/wice-*'

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
build/
.vscode/

15
Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM golang:1.16-alpine AS builder
WORKDIR /app
COPY go.mod ./
COPY go.sum ./
RUN go mod download
COPY wice/ ./wice/
RUN go build -o build/wice ./wice
FROM scratch
COPY --from=builder /app/build/wice /
CMD [ "/wice" ]

202
LICENSE Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

26
Makefile Normal file
View File

@@ -0,0 +1,26 @@
export GO111MODULE=on
TOOLS := $(notdir $(wildcard cmd/*))
OS ?= $(shell go env GOOS)
ARCH ?= $(shell go env GOARCH)
ALL_ARCH := amd64 arm arm64
ALL_OS := linux freebsd openbsd darwin windows
BINS := $(foreach X,$(ALL_OS),$(foreach Y,$(ALL_ARCH),wice-$X-$Y))
temp = $(subst -, ,$@)
cmd = $(word 1, $(temp))
os = $(word 2, $(temp))
arch = $(word 3, $(temp))
all: wice-$(OS)-$(ARCH)
release: $(PLATFORMS)
$(BINS):
GOOS=$(os) \
GOARCH=$(arch) \
go build -o 'build/$(cmd)-$(os)-$(arch)' ./cmd/$(cmd)
.PHONY: release $(PLATFORMS)

70
README.md Normal file
View File

@@ -0,0 +1,70 @@
# WICE - Wireguard Interactive Connectivity Establishment
[![Go Reference](https://pkg.go.dev/badge/github.com/stv0g/wice.svg)](https://pkg.go.dev/github.com/stv0g/wice)
![](https://img.shields.io/snyk/vulnerabilities/github/stv0g/wice)
[![](https://img.shields.io/github/checks-status/stv0g/wice/master)](https://github.com/stv0g/wice/actions)
[![](https://img.shields.io/librariesio/release/stv0g/wice)](https://libraries.io/github/stv0g/wice)
[![GitHub](https://img.shields.io/github/license/stv0g/wice)](https://github.com/stv0g/wice/blob/master/LICENSE)
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/stv0g/wice)
WICE is a userspace daemon managing Wireguard interfaces to establish peer-to-peer connections in harsh network environments.
It relies on the [awesome](https://github.com/pion/awesome-pion) [pion/ice] package for the interactive connectivity establishment as well as bundles the Go userspace implementation of Wiguard in a single binary for environments in which Wireguard kernel support has not landed yet.
## Getting started
To use WICE you first need to setup a signalling server:
1. Install WICE: `go get riasc.eu/wice`
2. Run the signalling server on a publicly accessible node: `wice-signal-http -port 8080`
Afterwards perform the following steps on each node which should join the mesh:
1. Install WICE: `go get riasc.eu/wice`
2. Configure your Wireguard interfaces using `wg`, `wg-quick` or [NetworkManager](https://blogs.gnome.org/thaller/2019/03/15/wireguard-in-networkmanager/)
3. Start the WICE daemon by running: `sudo wice -backend http://signalling-server:8080`
The WICE daemons will now attempt to discover valid endpoint addresses using the ICE protocol (e.g. contacting STUN servers).
These _ICE candidates_ are then exchanged via the signalling server and WICE will update the endpoint addresses of the Wireguard peers accordingly.
Once this has been done, the WICE logs should show `Connected to peer`.
## Documentation
Documentation of WICE can be found in the [`docs/`](./docs) directory.
## Authors
- Steffen Vogel ([@stv0g](https://github.com/stv0g), Institute for Automation of Complex Power Systems, RWTH Aachen University)
## Funding acknowledment
![](https://erigrid2.eu/wp-content/uploads/2020/03/europa_flag_low.jpg) The development of [WICE] has been supported by the [ERIGrid 2.0] project of the H2020 Programme under [Grant Agreement No. 870620](https://cordis.europa.eu/project/id/870620)
[Wireguard]: https://wireguard.com
[wireguard-go]: https://git.zx2c4.com/wireguard-go
[pion/ice]: https://github.com/pion/ice
[ICE]: https://datatracker.ietf.org/doc/html/rfc8445
[ICE-PAC]: https://datatracker.ietf.org/doc/html/rfc8863
[ICE-TCP]: https://datatracker.ietf.org/doc/html/rfc6544
[Trickle ICE]: https://datatracker.ietf.org/doc/html/rfc8838
[ICE-SDP]: https://datatracker.ietf.org/doc/html/rfc8839
[TURN-TCP]: https://datatracker.ietf.org/doc/html/rfc6062
[TURN-STUN]: https://datatracker.ietf.org/doc/html/rfc8656
[STUN]: https://datatracker.ietf.org/doc/html/rfc8489
[SDP]: https://datatracker.ietf.org/doc/html/rfc8866
[SDP-Offer-Answer]: https://datatracker.ietf.org/doc/html/rfc3264
[JWS]: https://datatracker.ietf.org/doc/html/rfc7515
[JWS-CT]: https://tools.ietf.org/id/draft-jordan-jws-ct-02.html
[JCS]: https://datatracker.ietf.org/doc/html/rfc8785
[WICE]: https://github.com/stv0g/wice
[ERIGrid 2.0]: https://erigrid2.eu
[NetworkManager]: https://github.com/max-moser/network-manager-wireguard
[systemd-networkd]: https://www.freedesktop.org/software/systemd/man/systemd.netdev.html#%5BWireGuard%5D%20Section%20Options
[wg-quick]: https://manpages.debian.org/unstable/wireguard-tools/wg-quick.8.en.html
[kilo]: https://kilo.squat.ai
[Nftables]: https://www.netfilter.org/projects/nftables/manpage.html
[XEdDSA]: https://signal.org/docs/specifications/xeddsa/
https://riyazali.net/posts/berkeley-packet-filter-in-golang/
https://squidarth.com/networking/systems/rc/2018/05/28/using-raw-sockets.html

View File

@@ -0,0 +1,70 @@
package main
import (
"encoding/hex"
"fmt"
"net"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/asm"
netx "riasc.eu/wice/internal/net"
)
const (
StunMagicCookie uint32 = 0x2112A442
)
func main() {
la := net.UDPAddr{
IP: net.IPv4zero,
Port: 12345,
}
spec := ebpf.ProgramSpec{
Type: ebpf.SocketFilter,
License: "GPL",
Instructions: asm.Instructions{
asm.Mov.Reg(asm.R6, asm.R1), // LDABS requires ctx in R6
asm.LoadAbs(-0x100000+22, asm.Half),
asm.JNE.Imm(asm.R0, int32(la.Port), "skip"),
asm.LoadAbs(-0x100000+32, asm.Word),
asm.JNE.Imm(asm.R0, int32(StunMagicCookie), "skip"),
asm.Mov.Imm(asm.R0, -1).Sym("exit"),
asm.Return(),
asm.Mov.Imm(asm.R0, 0).Sym("skip"),
asm.Return(),
},
}
fmt.Printf("Instructions:\n%v\n", spec.Instructions)
prog, err := ebpf.NewProgramWithOptions(&spec, ebpf.ProgramOptions{
LogLevel: 6, // TODO take configured log-level from args
})
if err != nil {
panic(err)
}
fuc, err := netx.NewFilteredUDPConn(la)
if err != nil {
panic(err)
}
err = fuc.ApplyFilter(prog)
if err != nil {
panic(err)
}
buf := make([]byte, 1024)
for {
n, ra, err := fuc.ReadFrom(buf)
if err != nil {
panic(err)
}
fmt.Printf("Bytes: %d\n", n)
fmt.Printf("RA: %+v\n", ra)
fmt.Printf("Bytes: %s\n", hex.EncodeToString(buf[:n]))
fmt.Println()
}
}

View File

@@ -0,0 +1,31 @@
package main
import (
"fmt"
"github.com/pion/stun"
)
func main() {
// Creating a "connection" to STUN server.
c, err := stun.Dial("udp", "127.0.0.1:12345")
if err != nil {
panic(err)
}
// Building binding request with random transaction id.
message := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
// Sending request to STUN server, waiting for response message.
if err := c.Do(message, func(res stun.Event) {
if res.Error != nil {
panic(res.Error)
}
// Decoding XOR-MAPPED-ADDRESS attribute from message.
var xorAddr stun.XORMappedAddress
if err := xorAddr.GetFrom(res.Message); err != nil {
panic(err)
}
fmt.Println("your IP is", xorAddr.IP)
}); err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,88 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
log "github.com/sirupsen/logrus"
"riasc.eu/wice/pkg/backend"
"riasc.eu/wice/pkg/crypto"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
)
var offers map[crypto.Key]map[crypto.Key]backend.Offer
func candidateHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
ourPKStr := vars["ours"]
theirPKStr := vars["theirs"]
ourPKStrUnescaped, _ := url.PathUnescape(ourPKStr)
theirPKStrUnescaped, _ := url.PathUnescape(theirPKStr)
var err error
var ourPK, theirPK crypto.Key
ourPK, err = crypto.ParseKey(ourPKStrUnescaped)
if err != nil {
http.Error(w, fmt.Sprintf("failed to parse key: %s", err), http.StatusBadRequest)
}
theirPK, err = crypto.ParseKey(theirPKStrUnescaped)
if err != nil {
http.Error(w, fmt.Sprintf("failed to parse key: %s", err), http.StatusBadRequest)
}
// Create missing maps and slice
if _, ok := offers[ourPK]; !ok {
offers[ourPK] = make(map[crypto.Key]backend.Offer)
}
if r.Method == "POST" {
dec := json.NewDecoder(r.Body)
var offer backend.Offer
err := dec.Decode(&offer)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
offers[ourPK][theirPK] = offer
} else if r.Method == "GET" {
offer, ok := offers[ourPK][theirPK]
if !ok {
http.Error(w, "Not found", http.StatusNotFound)
return
}
enc := json.NewEncoder(w)
enc.Encode(offer)
} else if r.Method == "DELETE" {
_, ok := offers[ourPK][theirPK]
if !ok {
http.Error(w, "Not found", http.StatusNotFound)
return
}
delete(offers[ourPK], theirPK)
}
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/offers/{ours}/{theirs}", candidateHandler)
lr := handlers.LoggingHandler(os.Stdout, r)
http.Handle("/", lr)
offers = make(map[crypto.Key]map[crypto.Key]backend.Offer)
log.Fatal(http.ListenAndServe(":8080", nil))
}

View File

@@ -0,0 +1,137 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"bytes"
"log"
"net/http"
"time"
"github.com/gorilla/websocket"
)
const (
// Time allowed to write a message to the peer.
writeWait = 10 * time.Second
// Time allowed to read the next pong message from the peer.
pongWait = 60 * time.Second
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
// Maximum message size allowed from peer.
maxMessageSize = 512
)
var (
newline = []byte{'\n'}
space = []byte{' '}
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
// Client is a middleman between the websocket connection and the hub.
type Client struct {
hub *Hub
// The websocket connection.
conn *websocket.Conn
// Buffered channel of outbound messages.
send chan []byte
}
// readPump pumps messages from the websocket connection to the hub.
//
// The application runs readPump in a per-connection goroutine. The application
// ensures that there is at most one reader on a connection by executing all
// reads from this goroutine.
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c
c.conn.Close()
}()
c.conn.SetReadLimit(maxMessageSize)
c.conn.SetReadDeadline(time.Now().Add(pongWait))
c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
for {
_, message, err := c.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("error: %v", err)
}
break
}
message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
c.hub.broadcast <- message
}
}
// writePump pumps messages from the hub to the websocket connection.
//
// A goroutine running writePump is started for each connection. The
// application ensures that there is at most one writer to a connection by
// executing all writes from this goroutine.
func (c *Client) writePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case message, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
// The hub closed the channel.
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
w, err := c.conn.NextWriter(websocket.TextMessage)
if err != nil {
return
}
w.Write(message)
// Add queued chat messages to the current websocket message.
n := len(c.send)
for i := 0; i < n; i++ {
w.Write(newline)
w.Write(<-c.send)
}
if err := w.Close(); err != nil {
return
}
case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
// serveWs handles websocket requests from the peer.
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
client.hub.register <- client
// Allow collection of memory referenced by the caller by doing all work in
// new goroutines.
go client.writePump()
go client.readPump()
}

53
cmd/wice-signal-ws/hub.go Normal file
View File

@@ -0,0 +1,53 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
// Hub maintains the set of active clients and broadcasts messages to the
// clients.
type Hub struct {
// Registered clients.
clients map[*Client]bool
// Inbound messages from the clients.
broadcast chan []byte
// Register requests from the clients.
register chan *Client
// Unregister requests from clients.
unregister chan *Client
}
func newHub() *Hub {
return &Hub{
broadcast: make(chan []byte),
register: make(chan *Client),
unregister: make(chan *Client),
clients: make(map[*Client]bool),
}
}
func (h *Hub) run() {
for {
select {
case client := <-h.register:
h.clients[client] = true
case client := <-h.unregister:
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
case message := <-h.broadcast:
for client := range h.clients {
select {
case client.send <- message:
default:
close(client.send)
delete(h.clients, client)
}
}
}
}
}

View File

@@ -0,0 +1,26 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"flag"
"log"
"net/http"
)
var addr = flag.String("addr", ":8080", "http service address")
func main() {
flag.Parse()
hub := newHub()
go hub.run()
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
serveWs(hub, w, r)
})
err := http.ListenAndServe(*addr, nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}

115
cmd/wice/main.go Normal file
View File

@@ -0,0 +1,115 @@
package main
import (
"math/rand"
"os"
"os/signal"
"syscall"
"time"
"github.com/bombsimon/logrusr"
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl"
"k8s.io/klog/v2"
"riasc.eu/wice/pkg/args"
be "riasc.eu/wice/pkg/backend"
"riasc.eu/wice/pkg/intf"
_ "riasc.eu/wice/pkg/backend/http"
_ "riasc.eu/wice/pkg/backend/k8s"
_ "riasc.eu/wice/pkg/backend/p2p"
)
func setupLogging() {
klogger := log.StandardLogger()
klogr := logrusr.NewLogger(klogger)
klog.SetLogger(klogr.WithName("k8s"))
log.SetFormatter(&log.TextFormatter{
ForceColors: true,
DisableQuote: true,
})
}
func setupRand() {
rand.Seed(time.Now().UTC().UnixNano())
}
func setupSignals() chan os.Signal {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
return ch
}
func main() {
setupLogging()
setupRand()
signals := setupSignals()
args, err := args.Parse(os.Args[0], os.Args[1:])
if err != nil {
log.WithError(err).Fatal("Failed to parse arguments")
}
if log.GetLevel() > log.DebugLevel {
args.DumpConfig(os.Stdout)
}
// Create backend
backend, err := be.NewBackend(args.Backend, args.BackendOptions)
if err != nil {
log.WithError(err).Fatal("Failed to initialize backend")
}
// Create Wireguard netlink socket
client, err := wgctrl.New()
if err != nil {
log.Fatal(err)
}
// Create interfaces
interfaces := &intf.Interfaces{}
defer interfaces.CloseAll()
interfaces.CreateFromArgs(client, backend, args)
events, errors, err := intf.WatchWireguardInterfaces()
if err != nil {
log.WithError(err).Error("Failed to watch interfaces")
return
}
log.Debug("Starting initial interface sync")
interfaces.SyncAll(client, backend, args)
ticker := time.NewTicker(args.WatchInterval)
out:
for {
select {
// We still a need periodic sync we can not (yet) monitor Wireguard interfaces
// for changes via a netlink socket (patch is pending)
case <-ticker.C:
log.Trace("Starting periodic interface sync")
interfaces.SyncAll(client, backend, args)
case event := <-events:
log.Trace("Received interface event: %s", event)
interfaces.SyncAll(client, backend, args)
case err := <-errors:
log.WithError(err).Error("Failed to watch for interface changes")
case sig := <-signals:
log.WithField("signal", sig).Debug("Received signal")
switch sig {
case syscall.SIGUSR1:
interfaces.SyncAll(client, backend, args)
default:
break out
}
}
}
}

5
cmd/wicectl/main.go Normal file
View File

@@ -0,0 +1,5 @@
package main
func main() {
// TODO
}

8
docs/Backends.md Normal file
View File

@@ -0,0 +1,8 @@
# Backends
WICE can support multiple backends for signalling session information such as session IDs, ICE candidates, public keys and STUN credentials.
## Available backends
Currently HTTP REST, Kubernetes and libp2p are supported as backends.
Checkout the `Backend` interface in `wice/backend/backend.go` for implementing your own backend.

29
docs/Design.md Normal file
View File

@@ -0,0 +1,29 @@
# Design
## Objectives
- Support [Trickle ICE]
- Support ICE restart
- Support [ICE-TCP]
- Sign and verify ICE offers with Wireguard keys (via [XEdDSA] signature scheme for Curve25519 key pairs)
- Seamless switch between ICE candidates and relays
- Zero configuration
- Eleviate users of exchaging endpoint IPs & ports
- Enables direct communication of Wireguard peers behind NAT / UDP-blocking firewalls
- Single-binary, zero dependency installation
- Bundled ICE agent & Wireguard userspace daemon
- Portablilty
- Support for user and kernel-space Wireguard implementations
- Zero performance impact
- Kernel-side filtering / redirection of Wireguard traffic
- Fallback to userspace proxying only if no Kernel features are available
- Minimized attack surface
- Drop privileges after inital configuration
- Compatible with existing Wireguard configuration utilities like:
- [NetworkManager]
- [systemd-networkd]
- [wg-quick]
- [kilo]
- Monitoring for new Wireguard interfaces and peers
- Inotify for new UAPI sockets in /var/run/wireguard
- Netlink subscription for link updates

64
docs/Proxying.md Normal file
View File

@@ -0,0 +1,64 @@
# Proxying
WICE implements multiple ways of running an ICE agent alongside Wireguard on the same UDP ports.
## Kernel Wireguard module
### Userspace Proxy
For each WG peer a new local UDP socket is opened.
WICE will update the endpoint address of the peer to this the local address of the new sockets.
Wireguard traffic is proxied by WICE between the local UDP and the ICE socket.
### RAW Sockets + BPF filter (Kernel)
We allocate a single RAW socket and assign a BPF filter to this socket which will only match STUN traffic to a specific UDP port.
UDP headers are parsed/produced by WICE.
WICE uses a UDPMux to mux all peers ICE Agents over this single RAW socket.
### NFtables port-redirection (Kernel)
Two [Netfilter] (nft) rules are added to filter input & output chains respectivly.
The input rule will match all non-STUN traffic directed at the local port of the ICE candidate and rewrites the UDP destination port to the local listen port of the Wireguard interface.
The output rule will mach all traffic originating from the listen port of the WG interface and directed to the port of the remote cadidate and rewrites the source port to the port of the local ICE candidate.
Wireguard traffic passes only through the Netfilter chains and remains inside the kernel.
Only STUN binding requests are passed to WICE.
```bash
$ sudo nft list ruleset
table inet wice {
chain ingress {
type filter hook input priority raw; policy accept;
udp dport 37281 @th,96,32 != 554869826 notrack udp dport set 1001
}
chain egress {
type filter hook output priority raw; policy accept;
udp sport 1001 udp dport 38767 notrack udp sport set 37281
}
}
```
## IPTables port-redirection
Similar to NFTables port-natting by using the legacy IPTables API.
## Userspace Wireguard implementation
### Userspace Proxy
Just like for the Kernel Wireguard module, a dedicated UDP socket for each WG peer is created.
WICE will update the endpoint address of the peer to this the local address of the new sockets.
Wireguard traffic is proxied by WICE between the local UDP and the ICE socket.
### In-process socket
WICE implements wireguard-go's `conn.Bind` interface to handle Wireguard's network IO.
Wireguard traffic is passed directly between `conn.Bind` and Pion's `ice.Conn`.
No round-trip through the kernel stack is required.
**Note:** This variant only works for the compiled-in version of wireguard-go in WICE.

69
docs/Signalling.md Normal file
View File

@@ -0,0 +1,69 @@
# Session signalling
Lets assume two Wireguard peers `Pa` & `Pb` are seeking to establish a ICE session.
The smaller public key (PK) of the two peers takes the role of the controlling agent.
In this example PA is the controlling agent: PK(PA) < PK(PB).
```
PA PB
--- initial offer --> id=SID_Pa, version=0, candidates=[], eoc=false
<-- initial offer --- id=SID_Pb, version=0, candidates=[], eoc=false
--- subsequent offers --> id=SID_Pa, version=1, candidates=[C1_Pa], eoc=false
<-- subsequent offers --- id=SID_Pb, version=1, candidates=[C1_Pb], eoc=false
--- subsequent offers --> id=SID_Pa, version=2, candidates=[C1_Pa, C2_Pa], eoc=false
<-- subsequent offers --- id=SID_Pb, version=2, candidates=[C1_Pb, C2_Pb], eoc=false
--- eoc. offer --> id=SID_Pa, version=3, candidates=[C1_Pa, C2_Pa], eoc=true
<-- eoc. offer --- id=SID_Pb, version=3, candidates=[C1_Pb, C2_Pb], eoc=true
```
## Restart
Agent will restart
- if
- `last_recv.id` has been set
- `recv.id!=last_recv.id`
- `recv.version==0`
- then
- set
- `local.id=rand()`
- `local.version=0`
- `local.candidates=[]`
- publish new offer
- wait for first offer including candidates from remote
- (re)start agent
- add first received
- start gathering candidates
- send an offers for each candidate `c`:
- `candidates=local.candidates.append(c)`
- `id=local.id`
- `rid=local.rid`
- `version=local.version++`
## Offer
Offers are encoded as JSON:
```json
{
"id": 1232353452, // Unique session id
"version": 0, // Session version, incremented with each updated offer
"cands": [ // List of ICE candidates
{
"type": "host",
"foundation": "1742129347",
"component": 1,
"network": "udp4",
"priority": 2130706431,
"address": "10.2.0.11",
"port": 37518
}
],
"eoc": false // Flag to indicate that all candidates have been gathered (ICE trickle)
}
```

35
docs/ToDo.md Normal file
View File

@@ -0,0 +1,35 @@
# TODOs
- [ ] Sign published candidates with XEdDSA signatures
- [ ] Add peer discovery
- [ ] Add libp2p backend
- Separate code into multiple repos:
- [ ] XEdDSA
- Contribute code into existing packages
- [ ] Watch for interfaces in wgctrl
- [ ] Single socket per Wireguard interface / ICE Agent
- Pass traffic in-process between userspace Wireguard and ICE sockets
- Use Wireguard-go's conn.Bind interface
- [ ] Single eBPF program per network NS to steer STUN traffic to ICE Agents
- https://ebpf.io/summit-2020-slides/eBPF_Summit_2020-Lightning-Jakub_Sitnicki-Steering_connections_to_sockets_with_BPF_socke_lookup_hook.pdf
- [ ] Use in-process pipe for wireguard-go's UAPI
- [ ] Update proxy instances instead of recreating them.
- Avoids possible packet loss during change of candidate pairs
- [ ] Use pion/ice's udpmux for creating a RAW socket sharing
- Sharing the same port as Wireguard kernel interface
- Use BPF filters for filtering STUN-only traffic
- [ ] Add better proxy implementations for OpenBSD, FreeBSD, Android and Windows
- [ ] Test co-existance of multipe `wice` instances
- nft tables might collide
- [ ] Use netlink multicast subscription for notification of Wireguard peer changes
- https://lore.kernel.org/patchwork/patch/1366219/
- [ ] Use netlink multicast group RTMGRP_LINK to for notification of new Wireguard interfaces
- [ ] Add links to code in README
- [ ] Add `XEdDSA` and `VXEdDSA` signature schemes to [JOSE IANA alg registry](https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms)
- [ ] Add wicectl command for controlling `wice` deaemon:
- `wicectl show [[INTF] [PEER]]`
- `wicectl add INTF`
- `wicectl del INTF`
- `wicectl discover INTF GROUP`
- `wicectl sync [INTF]`
- `wicectl restart INTF PEER`

13
docs/Usage.md Normal file
View File

@@ -0,0 +1,13 @@
# Usage
## Daemon
TODO
## HTTP Signalling Server
TODO
## WebSocket Signalling Server
TODO

15
docs/UseCases.md Normal file
View File

@@ -0,0 +1,15 @@
# Use-cases
## Zero-configuration
**Invocation:** `wice`
## Start user-space wireguard daemon
**Invocation:** `wice wg1`
## Peer discovery
**Note:** Not implemented yet
**Invocation:** `wice -discover -backend p2p://my_rendezvouz_phrase`

161
go.mod Normal file
View File

@@ -0,0 +1,161 @@
module riasc.eu/wice
go 1.17
require (
github.com/Scratch-net/vxeddsa v0.0.0-20180216190124-07c00d1c9bf7
github.com/bombsimon/logrusr v1.1.0
github.com/cilium/ebpf v0.6.2
github.com/fsnotify/fsnotify v1.4.9
github.com/google/gopacket v1.1.19
github.com/google/nftables v0.0.0-20210514154851-a285acebcad3
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.4.2
github.com/ipfs/go-cid v0.0.7
github.com/libp2p/go-libp2p v0.14.4
github.com/libp2p/go-libp2p-core v0.8.5
github.com/libp2p/go-libp2p-kad-dht v0.12.2
github.com/multiformats/go-multiaddr v0.3.3
github.com/multiformats/go-multihash v0.0.15
github.com/pion/dtls/v2 v2.0.9
github.com/pion/ice/v2 v2.1.8
github.com/pion/logging v0.2.2
github.com/pion/stun v0.3.5
github.com/pion/transport v0.12.3
github.com/sirupsen/logrus v1.8.1
github.com/ucarion/jcs v0.1.2
github.com/vishvananda/netlink v1.1.0
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
golang.zx2c4.com/wireguard v0.0.0-20210624150102-15b24b6179e0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210506160403-92e472f520a5
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/api v0.21.3
k8s.io/apimachinery v0.21.3
k8s.io/client-go v0.21.3
k8s.io/klog/v2 v2.8.0
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/briandowns/simple-httpd v0.5.0 // indirect
github.com/btcsuite/btcd v0.21.0-beta // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/flynn/noise v1.0.0 // indirect
github.com/go-logr/logr v0.4.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.4.3 // indirect
github.com/google/go-cmp v0.5.5 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/google/uuid v1.2.0 // indirect
github.com/googleapis/gnostic v0.4.1 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/huin/goupnp v1.0.0 // indirect
github.com/imdario/mergo v0.3.5 // indirect
github.com/ipfs/go-datastore v0.4.5 // indirect
github.com/ipfs/go-ipfs-util v0.0.2 // indirect
github.com/ipfs/go-ipns v0.0.2 // indirect
github.com/ipfs/go-log v1.0.5 // indirect
github.com/ipfs/go-log/v2 v2.1.3 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
github.com/jbenet/goprocess v0.1.4 // indirect
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 // indirect
github.com/json-iterator/go v1.1.10 // indirect
github.com/klauspost/cpuid/v2 v2.0.4 // indirect
github.com/koneu/natend v0.0.0-20150829182554-ec0926ea948d // indirect
github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d // indirect
github.com/libp2p/go-addr-util v0.0.2 // indirect
github.com/libp2p/go-buffer-pool v0.0.2 // indirect
github.com/libp2p/go-cidranger v1.1.0 // indirect
github.com/libp2p/go-conn-security-multistream v0.2.1 // indirect
github.com/libp2p/go-eventbus v0.2.1 // indirect
github.com/libp2p/go-flow-metrics v0.0.3 // indirect
github.com/libp2p/go-libp2p-asn-util v0.0.0-20200825225859-85005c6cf052 // indirect
github.com/libp2p/go-libp2p-autonat v0.4.2 // indirect
github.com/libp2p/go-libp2p-blankhost v0.2.0 // indirect
github.com/libp2p/go-libp2p-circuit v0.4.0 // indirect
github.com/libp2p/go-libp2p-discovery v0.5.0 // indirect
github.com/libp2p/go-libp2p-kbucket v0.4.7 // indirect
github.com/libp2p/go-libp2p-mplex v0.4.1 // indirect
github.com/libp2p/go-libp2p-nat v0.0.6 // indirect
github.com/libp2p/go-libp2p-noise v0.2.0 // indirect
github.com/libp2p/go-libp2p-peerstore v0.2.7 // indirect
github.com/libp2p/go-libp2p-pnet v0.2.0 // indirect
github.com/libp2p/go-libp2p-record v0.1.3 // indirect
github.com/libp2p/go-libp2p-swarm v0.5.0 // indirect
github.com/libp2p/go-libp2p-tls v0.1.3 // indirect
github.com/libp2p/go-libp2p-transport-upgrader v0.4.2 // indirect
github.com/libp2p/go-libp2p-yamux v0.5.4 // indirect
github.com/libp2p/go-maddr-filter v0.1.0 // indirect
github.com/libp2p/go-mplex v0.3.0 // indirect
github.com/libp2p/go-msgio v0.0.6 // indirect
github.com/libp2p/go-nat v0.0.5 // indirect
github.com/libp2p/go-netroute v0.1.6 // indirect
github.com/libp2p/go-openssl v0.0.7 // indirect
github.com/libp2p/go-reuseport v0.0.2 // indirect
github.com/libp2p/go-reuseport-transport v0.0.4 // indirect
github.com/libp2p/go-sockaddr v0.1.1 // indirect
github.com/libp2p/go-stream-muxer-multistream v0.3.0 // indirect
github.com/libp2p/go-tcp-transport v0.2.4 // indirect
github.com/libp2p/go-ws-transport v0.4.0 // indirect
github.com/libp2p/go-yamux/v2 v2.2.0 // indirect
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mdlayher/genetlink v1.0.0 // indirect
github.com/mdlayher/netlink v1.4.0 // indirect
github.com/miekg/dns v1.1.41 // indirect
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/multiformats/go-base32 v0.0.3 // indirect
github.com/multiformats/go-base36 v0.1.0 // indirect
github.com/multiformats/go-multiaddr-dns v0.3.1 // indirect
github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect
github.com/multiformats/go-multiaddr-net v0.2.0 // indirect
github.com/multiformats/go-multibase v0.0.3 // indirect
github.com/multiformats/go-multistream v0.2.2 // indirect
github.com/multiformats/go-varint v0.0.6 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pion/mdns v0.0.5 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/turn/v2 v2.0.5 // indirect
github.com/pion/udp v0.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.10.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.18.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect
github.com/whyrusleeping/multiaddr-filter v0.0.0-20160516205228-e903e4adabd7 // indirect
go.opencensus.io v0.23.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.16.0 // indirect
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect
golang.org/x/text v0.3.6 // indirect
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.5 // indirect
google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/utils v0.0.0-20201110183641-67b214c5f920 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect
)

1492
go.sum Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,286 @@
package ice
// Based on https://github.com/pion/ice/blob/v2.1.8/udp_mux.go
import (
"io"
"net"
"os"
"strings"
"sync"
"github.com/pion/stun"
log "github.com/sirupsen/logrus"
netx "riasc.eu/wice/internal/net"
)
const (
receiveMTU = 8192
)
// UDPMuxDefault is an implementation of the interface
type FilteredUDPMux struct {
params FilteredUDPMuxParams
closedChan chan struct{}
closeOnce sync.Once
// conns is a map of all udpMuxedConn indexed by ufrag|network|candidateType
conns map[string]*udpMuxedConn
addressMapMu sync.RWMutex
addressMap map[string]*udpMuxedConn
// buffer pool to recycle buffers for net.UDPAddr encodes/decodes
pool *sync.Pool
mu sync.Mutex
}
const maxAddrSize = 512
// UDPMuxParams are parameters for UDPMux.
type FilteredUDPMuxParams struct {
Logger *log.Entry
Conn *netx.FilteredUDPConn
}
// NewUDPMuxDefault creates an implementation of UDPMux
func NewFilteredUDPMux(params FilteredUDPMuxParams) *FilteredUDPMux {
if params.Logger == nil {
params.Logger = log.WithField("logger", "ice-mux")
}
m := &FilteredUDPMux{
addressMap: map[string]*udpMuxedConn{},
params: params,
conns: make(map[string]*udpMuxedConn),
closedChan: make(chan struct{}, 1),
pool: &sync.Pool{
New: func() interface{} {
// big enough buffer to fit both packet and address
return newBufferHolder(receiveMTU + maxAddrSize)
},
},
}
go m.connWorker()
return m
}
// LocalAddr returns the listening address of this UDPMuxDefault
func (m *FilteredUDPMux) LocalAddr() net.Addr {
return m.params.Conn.LocalAddr()
}
// GetConn returns a PacketConn given the connection's ufrag and network
// creates the connection if an existing one can't be found
func (m *FilteredUDPMux) GetConn(ufrag string) (net.PacketConn, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.IsClosed() {
return nil, io.ErrClosedPipe
}
if c, ok := m.conns[ufrag]; ok {
return c, nil
}
c := m.createMuxedConn(ufrag)
go func() {
<-c.CloseChannel()
m.removeConn(ufrag)
}()
m.conns[ufrag] = c
return c, nil
}
// RemoveConnByUfrag stops and removes the muxed packet connection
func (m *FilteredUDPMux) RemoveConnByUfrag(ufrag string) {
m.mu.Lock()
removedConns := make([]*udpMuxedConn, 0)
for key := range m.conns {
if key != ufrag {
continue
}
c := m.conns[key]
delete(m.conns, key)
if c != nil {
removedConns = append(removedConns, c)
}
}
// keep lock section small to avoid deadlock with conn lock
m.mu.Unlock()
m.addressMapMu.Lock()
defer m.addressMapMu.Unlock()
for _, c := range removedConns {
addresses := c.getAddresses()
for _, addr := range addresses {
delete(m.addressMap, addr)
}
}
}
// IsClosed returns true if the mux had been closed
func (m *FilteredUDPMux) IsClosed() bool {
select {
case <-m.closedChan:
return true
default:
return false
}
}
// Close the mux, no further connections could be created
func (m *FilteredUDPMux) Close() error {
m.params.Logger.Info("Closing mux")
var err error
m.closeOnce.Do(func() {
m.mu.Lock()
defer m.mu.Unlock()
for _, c := range m.conns {
_ = c.Close()
}
m.conns = make(map[string]*udpMuxedConn)
close(m.closedChan)
})
return err
}
func (m *FilteredUDPMux) removeConn(key string) {
m.mu.Lock()
c := m.conns[key]
delete(m.conns, key)
// keep lock section small to avoid deadlock with conn lock
m.mu.Unlock()
if c == nil {
return
}
m.addressMapMu.Lock()
defer m.addressMapMu.Unlock()
addresses := c.getAddresses()
for _, addr := range addresses {
delete(m.addressMap, addr)
}
}
func (m *FilteredUDPMux) writeTo(buf []byte, raddr net.Addr) (n int, err error) {
return m.params.Conn.WriteTo(buf, raddr)
}
func (m *FilteredUDPMux) registerConnForAddress(conn *udpMuxedConn, addr string) {
if m.IsClosed() {
return
}
m.addressMapMu.Lock()
defer m.addressMapMu.Unlock()
existing, ok := m.addressMap[addr]
if ok {
existing.removeAddress(addr)
}
m.addressMap[addr] = conn
m.params.Logger.Debugf("Registered %s for %s", addr, conn.params.Key)
}
func (m *FilteredUDPMux) createMuxedConn(key string) *udpMuxedConn {
c := newUDPMuxedConn(&udpMuxedConnParams{
Mux: m,
Key: key,
AddrPool: m.pool,
LocalAddr: m.LocalAddr(),
Logger: m.params.Logger,
})
return c
}
func (m *FilteredUDPMux) connWorker() {
logger := m.params.Logger
defer func() {
_ = m.Close()
}()
buf := make([]byte, receiveMTU)
for {
n, addr, err := m.params.Conn.ReadFrom(buf)
if m.IsClosed() {
return
} else if err != nil {
if os.IsTimeout(err) {
continue
} else if err != io.EOF {
logger.Errorf("could not read udp packet: %v", err)
}
return
}
udpAddr, ok := addr.(*net.UDPAddr)
if !ok {
logger.Errorf("underlying PacketConn did not return a UDPAddr")
return
}
// If we have already seen this address dispatch to the appropriate destination
m.addressMapMu.Lock()
destinationConn := m.addressMap[addr.String()]
m.addressMapMu.Unlock()
// If we haven't seen this address before but is a STUN packet lookup by ufrag
if destinationConn == nil && stun.IsMessage(buf[:n]) {
msg := &stun.Message{
Raw: append([]byte{}, buf[:n]...),
}
if err = msg.Decode(); err != nil {
m.params.Logger.Warnf("Failed to handle decode ICE from %s: %v\n", addr.String(), err)
continue
}
attr, stunAttrErr := msg.Get(stun.AttrUsername)
if stunAttrErr != nil {
m.params.Logger.Warnf("No Username attribute in STUN message from %s\n", addr.String())
continue
}
ufrag := strings.Split(string(attr), ":")[0]
m.mu.Lock()
destinationConn = m.conns[ufrag]
m.mu.Unlock()
}
if destinationConn == nil {
m.params.Logger.Tracef("Dropping packet from %s, addr: %s", udpAddr.String(), addr.String())
continue
}
if err = destinationConn.writePacket(buf[:n], udpAddr); err != nil {
m.params.Logger.Errorf("Could not write packet: %v", err)
}
}
}
type bufferHolder struct {
buffer []byte
}
func newBufferHolder(size int) *bufferHolder {
return &bufferHolder{
buffer: make([]byte, size),
}
}

52
internal/ice/log.go Normal file
View File

@@ -0,0 +1,52 @@
package ice
import (
"strings"
"github.com/pion/logging"
log "github.com/sirupsen/logrus"
)
type LoggerFactory struct {
}
type Logger struct {
log.Entry
}
func capitalize(msg string) string {
for i, v := range msg {
return strings.ToUpper(string(v)) + msg[i+1:]
}
return ""
}
func (l *Logger) Debug(msg string) {
l.Entry.Debug(capitalize(msg))
}
func (l *Logger) Error(msg string) {
l.Entry.Error(capitalize(msg))
}
func (l *Logger) Info(msg string) {
l.Entry.Info(capitalize(msg))
}
func (l *Logger) Trace(msg string) {
l.Entry.Trace(capitalize(msg))
}
func (l *Logger) Warn(msg string) {
l.Entry.Warn(capitalize(msg))
}
func (f *LoggerFactory) NewLogger(scope string) logging.LeveledLogger {
logger := &Logger{
Entry: *log.WithFields(log.Fields{
"logger": scope,
}),
}
return logger
}

124
internal/ice/net.go Normal file
View File

@@ -0,0 +1,124 @@
package ice
import (
"fmt"
"net"
"os"
"github.com/pion/transport/vnet"
)
// Net represents a local network stack euivalent to a set of layers from NIC
// up to the transport (UDP / TCP) layer.
type Net struct {
ifs []*vnet.Interface
}
type UDPPacketConn struct {
net.PacketConn
}
func (c *UDPPacketConn) Read(b []byte) (int, error) {
return 0, nil
}
func (c *UDPPacketConn) RemoteAddr() net.Addr {
return &net.IPAddr{}
}
func (c *UDPPacketConn) Write(b []byte) (int, error) {
return 0, nil
}
func NewNet() *Net {
ifs := []*vnet.Interface{}
if orgIfs, err := net.Interfaces(); err == nil {
for _, orgIfc := range orgIfs {
ifc := vnet.NewInterface(orgIfc)
if addrs, err := orgIfc.Addrs(); err == nil {
for _, addr := range addrs {
ifc.AddAddr(addr)
}
}
ifs = append(ifs, ifc)
}
}
return &Net{ifs: ifs}
}
// Interfaces returns a list of the system's network interfaces.
func (n *Net) Interfaces() ([]*vnet.Interface, error) {
return n.ifs, nil
}
// InterfaceByName returns the interface specified by name.
func (n *Net) InterfaceByName(name string) (*vnet.Interface, error) {
for _, ifc := range n.ifs {
if ifc.Name == name {
return ifc, nil
}
}
return nil, fmt.Errorf("interface %s: %w", name, os.ErrNotExist)
}
// ListenPacket announces on the local network address.
func (n *Net) ListenPacket(network string, address string) (net.PacketConn, error) {
return net.ListenPacket(network, address)
}
// ListenUDP acts like ListenPacket for UDP networks.
func (n *Net) ListenUDP(network string, locAddr *net.UDPAddr) (vnet.UDPPacketConn, error) {
return net.ListenUDP(network, locAddr)
}
// Dial connects to the address on the named network.
func (n *Net) Dial(network, address string) (net.Conn, error) {
return net.Dial(network, address)
}
// CreateDialer creates an instance of vnet.Dialer
func (n *Net) CreateDialer(dialer *net.Dialer) Dialer {
return &vDialer{
dialer: dialer,
}
}
// DialUDP acts like Dial for UDP networks.
func (n *Net) DialUDP(network string, laddr, raddr *net.UDPAddr) (vnet.UDPPacketConn, error) {
conn, err := net.DialUDP(network, laddr, raddr)
if err != nil {
return nil, err
}
return &UDPPacketConn{
PacketConn: conn,
}, nil
}
// ResolveUDPAddr returns an address of UDP end point.
func (n *Net) ResolveUDPAddr(network, address string) (*net.UDPAddr, error) {
return net.ResolveUDPAddr(network, address)
}
// IsVirtual tests if the virtual network is enabled.
func (n *Net) IsVirtual() bool {
return false
}
// Dialer is identical to net.Dialer excepts that its methods
// (Dial, DialContext) are overridden to use virtual network.
// Use vnet.CreateDialer() to create an instance of this Dialer.
type Dialer interface {
Dial(network, address string) (net.Conn, error)
}
type vDialer struct {
dialer *net.Dialer
}
func (d *vDialer) Dial(network, address string) (net.Conn, error) {
return d.dialer.Dial(network, address)
}

View File

@@ -0,0 +1,249 @@
package ice
// Copied from https://github.com/pion/ice/blob/v2.1.8/udp_muxed_conn.go
import (
"encoding/binary"
"io"
"net"
"sync"
"time"
"github.com/pion/transport/packetio"
log "github.com/sirupsen/logrus"
)
type udpMuxedConnParams struct {
Mux *FilteredUDPMux
AddrPool *sync.Pool
Key string
LocalAddr net.Addr
Logger *log.Entry
}
// udpMuxedConn represents a logical packet conn for a single remote as identified by ufrag
type udpMuxedConn struct {
params *udpMuxedConnParams
// remote addresses that we have sent to on this conn
addresses []string
// channel holding incoming packets
buffer *packetio.Buffer
closedChan chan struct{}
closeOnce sync.Once
mu sync.Mutex
}
func newUDPMuxedConn(params *udpMuxedConnParams) *udpMuxedConn {
p := &udpMuxedConn{
params: params,
buffer: packetio.NewBuffer(),
closedChan: make(chan struct{}),
}
return p
}
func (c *udpMuxedConn) ReadFrom(b []byte) (n int, raddr net.Addr, err error) {
buf := c.params.AddrPool.Get().(*bufferHolder)
defer c.params.AddrPool.Put(buf)
// read address
total, err := c.buffer.Read(buf.buffer)
if err != nil {
return 0, nil, err
}
dataLen := int(binary.LittleEndian.Uint16(buf.buffer[:2]))
if dataLen > total || dataLen > len(b) {
return 0, nil, io.ErrShortBuffer
}
// read data and then address
offset := 2
copy(b, buf.buffer[offset:offset+dataLen])
offset += dataLen
// read address len & decode address
addrLen := int(binary.LittleEndian.Uint16(buf.buffer[offset : offset+2]))
offset += 2
if raddr, err = decodeUDPAddr(buf.buffer[offset : offset+addrLen]); err != nil {
return 0, nil, err
}
return dataLen, raddr, nil
}
func (c *udpMuxedConn) WriteTo(buf []byte, raddr net.Addr) (n int, err error) {
if c.isClosed() {
return 0, io.ErrClosedPipe
}
// each time we write to a new address, we'll register it with the mux
addr := raddr.String()
if !c.containsAddress(addr) {
c.addAddress(addr)
}
return c.params.Mux.writeTo(buf, raddr)
}
func (c *udpMuxedConn) LocalAddr() net.Addr {
return c.params.LocalAddr
}
func (c *udpMuxedConn) SetDeadline(tm time.Time) error {
return nil
}
func (c *udpMuxedConn) SetReadDeadline(tm time.Time) error {
return nil
}
func (c *udpMuxedConn) SetWriteDeadline(tm time.Time) error {
return nil
}
func (c *udpMuxedConn) CloseChannel() <-chan struct{} {
return c.closedChan
}
func (c *udpMuxedConn) Close() error {
var err error
c.closeOnce.Do(func() {
err = c.buffer.Close()
close(c.closedChan)
})
c.mu.Lock()
defer c.mu.Unlock()
c.addresses = nil
return err
}
func (c *udpMuxedConn) isClosed() bool {
select {
case <-c.closedChan:
return true
default:
return false
}
}
func (c *udpMuxedConn) getAddresses() []string {
c.mu.Lock()
defer c.mu.Unlock()
addresses := make([]string, len(c.addresses))
copy(addresses, c.addresses)
return addresses
}
func (c *udpMuxedConn) addAddress(addr string) {
c.mu.Lock()
c.addresses = append(c.addresses, addr)
c.mu.Unlock()
// map it on mux
c.params.Mux.registerConnForAddress(c, addr)
}
func (c *udpMuxedConn) removeAddress(addr string) {
c.mu.Lock()
defer c.mu.Unlock()
newAddresses := make([]string, 0, len(c.addresses))
for _, a := range c.addresses {
if a != addr {
newAddresses = append(newAddresses, a)
}
}
c.addresses = newAddresses
}
func (c *udpMuxedConn) containsAddress(addr string) bool {
c.mu.Lock()
defer c.mu.Unlock()
for _, a := range c.addresses {
if addr == a {
return true
}
}
return false
}
func (c *udpMuxedConn) writePacket(data []byte, addr *net.UDPAddr) error {
// write two packets, address and data
buf := c.params.AddrPool.Get().(*bufferHolder)
defer c.params.AddrPool.Put(buf)
// format of buffer | data len | data bytes | addr len | addr bytes |
if len(buf.buffer) < len(data)+maxAddrSize {
return io.ErrShortBuffer
}
// data len
binary.LittleEndian.PutUint16(buf.buffer, uint16(len(data)))
offset := 2
// data
copy(buf.buffer[offset:], data)
offset += len(data)
// write address first, leaving room for its length
n, err := encodeUDPAddr(addr, buf.buffer[offset+2:])
if err != nil {
return nil
}
total := offset + n + 2
// address len
binary.LittleEndian.PutUint16(buf.buffer[offset:], uint16(n))
if _, err := c.buffer.Write(buf.buffer[:total]); err != nil {
return err
}
return nil
}
func encodeUDPAddr(addr *net.UDPAddr, buf []byte) (int, error) {
ipdata, err := addr.IP.MarshalText()
if err != nil {
return 0, err
}
total := 2 + len(ipdata) + 2 + len(addr.Zone)
if total > len(buf) {
return 0, io.ErrShortBuffer
}
binary.LittleEndian.PutUint16(buf, uint16(len(ipdata)))
offset := 2
n := copy(buf[offset:], ipdata)
offset += n
binary.LittleEndian.PutUint16(buf[offset:], uint16(addr.Port))
offset += 2
copy(buf[offset:], addr.Zone)
return total, nil
}
func decodeUDPAddr(buf []byte) (*net.UDPAddr, error) {
addr := net.UDPAddr{}
offset := 0
ipLen := int(binary.LittleEndian.Uint16(buf[:2]))
offset += 2
// basic bounds checking
if ipLen+offset > len(buf) {
return nil, io.ErrShortBuffer
}
if err := addr.IP.UnmarshalText(buf[offset : offset+ipLen]); err != nil {
return nil, err
}
offset += ipLen
addr.Port = int(binary.LittleEndian.Uint16(buf[offset : offset+2]))
offset += 2
zone := make([]byte, len(buf[offset:]))
copy(zone, buf[offset:])
addr.Zone = string(zone)
return &addr, nil
}

View File

@@ -0,0 +1,140 @@
package ice
import (
"encoding/hex"
"fmt"
"net"
"github.com/cilium/ebpf"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"golang.org/x/net/bpf"
"syscall"
log "github.com/sirupsen/logrus"
)
const (
SO_ATTACH_BPF int = 50
SO_ATTACH_FILTER int = 26
)
// Filter represents a classic BPF filter program that can be applied to a socket
type Filter []bpf.Instruction
type FilteredUDPConn struct {
fd int
localAddr net.UDPAddr
}
func (fuc *FilteredUDPConn) LocalAddr() net.Addr {
return &fuc.localAddr
}
func (fuc *FilteredUDPConn) ReadFrom(buf []byte) (n int, addr net.Addr, err error) {
n, rAddr, err := syscall.Recvfrom(fuc.fd, buf, 0)
if err != nil {
return -1, nil, err
}
rAddrIn4, ok := rAddr.(*syscall.SockaddrInet4)
if !ok {
return -1, nil, fmt.Errorf("invalid address type")
}
packet := gopacket.NewPacket(buf[:n], layers.LayerTypeIPv4, gopacket.DecodeOptions{
Lazy: true,
NoCopy: true,
})
transport := packet.TransportLayer()
if transport == nil {
return -1, nil, fmt.Errorf("failed to decode packet")
}
udp, ok := transport.(*layers.UDP)
if !ok {
return -1, nil, fmt.Errorf("invalid layer type")
}
payload := packet.ApplicationLayer()
rUDPAddr := &net.UDPAddr{
IP: rAddrIn4.Addr[:],
Port: int(udp.SrcPort),
}
n = len(payload.Payload())
copy(buf[:n], payload.Payload()[:])
log.Tracef("ReadFrom: ra=%s, len=%d, buf=%s", rUDPAddr, n, hex.EncodeToString(buf[:n]))
return n, rUDPAddr, nil
}
func (fuc *FilteredUDPConn) WriteTo(buf []byte, rAddr net.Addr) (n int, err error) {
rUDPAddr, ok := rAddr.(*net.UDPAddr)
if !ok {
return -1, fmt.Errorf("invalid address type")
}
rSockAddr := &syscall.SockaddrInet4{
Port: 0,
}
copy(rSockAddr.Addr[:], rUDPAddr.IP.To4())
buffer := gopacket.NewSerializeBuffer()
payload := gopacket.Payload(buf)
ip := &layers.IPv4{
Version: 4,
TTL: 64,
SrcIP: fuc.localAddr.IP,
DstIP: rUDPAddr.IP,
Protocol: layers.IPProtocolUDP,
}
udp := &layers.UDP{
SrcPort: layers.UDPPort(fuc.localAddr.Port),
DstPort: layers.UDPPort(rUDPAddr.Port),
}
udp.SetNetworkLayerForChecksum(ip)
seropts := gopacket.SerializeOptions{
ComputeChecksums: true,
FixLengths: true,
}
if err := gopacket.SerializeLayers(buffer, seropts, udp, payload); err != nil {
return -1, fmt.Errorf("failed serialize packet: %s", err)
}
syscall.Sendto(fuc.fd, buffer.Bytes(), 0, rSockAddr)
return 0, nil
}
func (fuc *FilteredUDPConn) ApplyFilter(prog *ebpf.Program) error {
// Attach filter program
if err := syscall.SetsockoptInt(fuc.fd, syscall.SOL_SOCKET, SO_ATTACH_BPF, prog.FD()); err != nil {
return fmt.Errorf("failed setsockopt(fd, SOL_SOCKET, SO_ATTACH_BPF): %w", err)
}
return nil
}
func (fuc *FilteredUDPConn) Close() error {
return nil // TODO
}
func NewFilteredUDPConn(lAddr net.UDPAddr) (fuc *FilteredUDPConn, err error) {
fuc = &FilteredUDPConn{
localAddr: lAddr,
}
// Open a raw socket
fuc.fd, err = syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_UDP)
if err != nil {
panic(err)
}
return fuc, nil
}

68
internal/util/util.go Normal file
View File

@@ -0,0 +1,68 @@
package util
import (
"bytes"
"encoding/base64"
"math/rand"
"net"
"syscall"
"unsafe"
)
type Less func(i, j int) bool
func CmpEndpoint(a, b *net.UDPAddr) int {
if a == nil && b == nil {
return 0
}
if (a != nil && b == nil) || (a == nil && b != nil) {
return 1
}
if !a.IP.Equal(b.IP) || a.Port != b.Port || a.Zone != b.Zone {
return 1
}
return 0
}
func CmpNet(a, b *net.IPNet) int {
cmp := bytes.Compare(a.Mask, b.Mask)
if cmp != 0 {
return cmp
}
return bytes.Compare(a.IP, b.IP)
}
// func lessNets(nets []net.IPNet) Less {
// return func(i, j int) bool { return cmpNet(&nets[i], &nets[j]) < 0 }
// }
// GenerateRandomBytes returns securely generated random bytes.
// It will return an error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func GenerateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
// Note that err == nil only if we read len(b) bytes.
if err != nil {
return nil, err
}
return b, nil
}
// GenerateRandomString returns a URL-safe, base64 encoded
// securely generated random string.
func GenerateRandomString(s int) (string, error) {
b, err := GenerateRandomBytes(s)
return base64.URLEncoding.EncodeToString(b), err
}
func SetsockoptBytes(fd int, level int, opt int, b []byte) syscall.Errno {
_, _, errno := syscall.Syscall6(syscall.SYS_SETSOCKOPT,
uintptr(fd), uintptr(level), uintptr(opt),
uintptr(unsafe.Pointer(&b[0])), uintptr(len(b)), 0)
return errno
}

View File

@@ -0,0 +1,73 @@
package util_test
import (
"net"
"testing"
"riasc.eu/wice/internal/util"
)
func TestCmpEndpointEqual(t *testing.T) {
a := net.UDPAddr{
IP: net.ParseIP("1.1.1.1"),
Port: 1,
}
if util.CmpEndpoint(&a, &a) != 0 {
t.Fail()
}
}
func TestCmpEndpointUnequal(t *testing.T) {
a := net.UDPAddr{
IP: net.ParseIP("1.1.1.1"),
Port: 1,
}
b := net.UDPAddr{
IP: net.ParseIP("2.2.2.2"),
Port: 1,
}
if util.CmpEndpoint(&a, &b) == 0 {
t.Fail()
}
}
func TestGenerateRandomBytes(t *testing.T) {
r, err := util.GenerateRandomBytes(16)
if err != nil {
t.Fail()
}
if len(r) != 16 {
t.Fail()
}
}
func TestCmpNetEqual(t *testing.T) {
_, a, err := net.ParseCIDR("1.1.1.1/0")
if err != nil {
t.Fail()
}
if util.CmpNet(a, a) != 0 {
t.Fail()
}
}
func TestCmpNetUnequal(t *testing.T) {
_, a, err := net.ParseCIDR("1.1.1.1/0")
if err != nil {
t.Fail()
}
_, b, err := net.ParseCIDR("1.1.1.1/1")
if err != nil {
t.Fail()
}
if util.CmpNet(a, b) == 0 {
t.Fail()
}
}

121
internal/wg/bind.go Normal file
View File

@@ -0,0 +1,121 @@
package wg
import (
"net"
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/conn"
)
type IceBind struct {
// Interface *intf.Interface
}
type IceEndpoint struct {
net.UDPAddr
// Peer *intf.Peer
String string
}
// clears the source address
func (ep *IceEndpoint) ClearSrc() {
log.Debugf("EP %s ClearSrc()", ep.String)
}
// returns the local source address (ip:port)
func (ep *IceEndpoint) SrcToString() string {
log.Debugf("EP %s SrcToString()", ep.String)
return ep.String + ":src"
}
// returns the destination address (ip:port)
func (ep *IceEndpoint) DstToString() string {
log.Debugf("EP %s DstToString()", ep.String)
return ep.String + ":dst"
}
// used for mac2 cookie calculations
func (ep *IceEndpoint) DstToBytes() []byte {
log.Debugf("EP %s DstToBytes()", ep.String)
return []byte(ep.String)
}
func (ep *IceEndpoint) DstIP() net.IP {
log.Debugf("EP %s DstIP()", ep.String)
return ep.IP
}
func (ep *IceEndpoint) SrcIP() net.IP {
log.Debugf("EP %s SrcIP()", ep.String)
return ep.IP
}
func NewIceBind() conn.Bind {
return &IceBind{
// Interface: i,
}
}
// Open puts the Bind into a listening state on a given port and reports the actual
// port that it bound to. Passing zero results in a random selection.
// fns is the set of functions that will be called to receive packets.
func (b *IceBind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err error) {
log.Debugf("Bind Open(port=%d)", port)
fns = append(fns, b.receive)
return fns, 0, nil
}
// Close closes the Bind listener.
// All fns returned by Open must return net.ErrClosed after a call to Close.
func (b *IceBind) Close() error {
log.Debug("Bind Close()")
return nil
}
// SetMark sets the mark for each packet sent through this Bind.
// This mark is passed to the kernel as the socket option SO_MARK.
func (b *IceBind) SetMark(mark uint32) error {
log.Debugf("Bind SetMark(mark=%d)", mark)
return nil // Stub
}
// Send writes a packet b to address ep.
func (b *IceBind) Send(buf []byte, ep conn.Endpoint) error {
log.Debugf("Bind Send(len=%d, ep=%s)", len(buf), ep.(*IceEndpoint).String)
return nil
}
// ParseEndpoint creates a new endpoint from a string.
func (b *IceBind) ParseEndpoint(s string) (ep conn.Endpoint, err error) {
log.Debugf("Bind ParseEndpoints(%s)", s)
addr, err := net.ResolveUDPAddr("udp", s)
if err != nil {
return &IceEndpoint{}, err
}
return &IceEndpoint{
UDPAddr: *addr,
String: s,
}, nil
}
func (b *IceBind) receive(buf []byte) (n int, ep conn.Endpoint, err error) {
log.Debug("Bind receive()")
buf[0] = 1
return 1, &IceEndpoint{}, nil
}

17
internal/wg/compare.go Normal file
View File

@@ -0,0 +1,17 @@
package wg
import (
"bytes"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
type Less func(i, j int) bool
func CmpPeers(a, b *wgtypes.Peer) int {
return bytes.Compare(a.PublicKey[:], b.PublicKey[:])
}
func LessPeers(peers []wgtypes.Peer) Less {
return func(i, j int) bool { return CmpPeers(&peers[i], &peers[j]) < 0 }
}

View File

@@ -0,0 +1,27 @@
package wg_test
import (
"testing"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"riasc.eu/wice/internal/wg"
)
func TestCmpPeersEqual(t *testing.T) {
a := wgtypes.Peer{}
b := wgtypes.Peer{}
if wg.CmpPeers(&a, &a) != 0 {
t.Fail()
}
var err error
b.PublicKey, err = wgtypes.GenerateKey()
if err != nil {
t.Fail()
}
if wg.CmpPeers(&a, &b) >= 0 {
t.Fail()
}
}

340
pkg/args/args.go Normal file
View File

@@ -0,0 +1,340 @@
package args
import (
"flag"
"fmt"
"io"
"net/url"
"os"
"regexp"
"strings"
"time"
log "github.com/sirupsen/logrus"
pice "riasc.eu/wice/internal/ice"
"riasc.eu/wice/pkg/backend"
"riasc.eu/wice/pkg/proxy"
"github.com/pion/ice/v2"
)
// Copied from pion/ice/agent_config.go
const (
// defaultCheckInterval is the interval at which the agent performs candidate checks in the connecting phase
defaultCheckInterval = 200 * time.Millisecond
// keepaliveInterval used to keep candidates alive
defaultKeepaliveInterval = 2 * time.Second
// defaultDisconnectedTimeout is the default time till an Agent transitions disconnected
defaultDisconnectedTimeout = 5 * time.Second
// defaultFailedTimeout is the default time till an Agent transitions to failed after disconnected
defaultFailedTimeout = 25 * time.Second
// max binding request before considering a pair failed
defaultMaxBindingRequests = 7
)
type arrayFlags []string
func (i *arrayFlags) String() string {
return strings.Join(*i, ",")
}
func (i *arrayFlags) Set(value string) error {
*i = append(*i, value)
return nil
}
type Args struct {
Backend *url.URL
BackendOptions map[string]string
User bool
ProxyType proxy.ProxyType
// Discover bool
ConfigSync bool
ConfigPath string
WatchInterval time.Duration
RestartInterval time.Duration
InterfaceRegex *regexp.Regexp
IceInterfaceRegex *regexp.Regexp
AgentConfig ice.AgentConfig
Interfaces []string
}
var yesno = map[bool]string{
true: "yes",
false: "no",
}
func showUsage() {
fmt.Fprintf(os.Stderr, "usage: %s [OPTIONS] [IFACES ...]\n", os.Args[0])
fmt.Println()
fmt.Println(" IFACES is a list of Wireguard interfaces")
fmt.Println(" (defaults to all available Wireguard interfaces)")
fmt.Println("")
fmt.Println(("Available OPTIONS are:"))
flag.PrintDefaults()
fmt.Println()
fmt.Println(" ** These options can be specified multiple times")
fmt.Println()
fmt.Println("Available backends types are:")
for name, plugin := range backend.Backends {
fmt.Printf(" %-7s %s\n", name, plugin.Description)
}
}
func (a *Args) DumpConfig(wr io.Writer) {
fmt.Fprintln(wr, "Options:")
fmt.Fprintln(wr, " URLs:")
for _, u := range a.AgentConfig.Urls {
fmt.Fprintf(wr, " %s\n", u.String())
}
fmt.Fprintln(wr, " Interfaces:")
for _, d := range a.Interfaces {
fmt.Fprintf(wr, " %s\n", d)
}
fmt.Fprintf(wr, " User: %s\n", yesno[a.User])
fmt.Fprintf(wr, " ProxyType: %s\n", a.ProxyType.String())
fmt.Fprintf(wr, " Backend: %s\n", a.Backend.String())
fmt.Fprintln(wr, " Backend options:")
for k := range a.BackendOptions {
fmt.Fprintf(wr, " %s=%s\n", k, a.BackendOptions[k])
}
}
func candidateTypeFromString(t string) (ice.CandidateType, error) {
switch t {
case "host":
return ice.CandidateTypeHost, nil
case "srflx":
return ice.CandidateTypeServerReflexive, nil
case "prflx":
return ice.CandidateTypePeerReflexive, nil
case "relay":
return ice.CandidateTypeRelay, nil
default:
return ice.CandidateTypeUnspecified, fmt.Errorf("unknown candidate type: %s", t)
}
}
func networkTypeFromString(t string) (ice.NetworkType, error) {
switch t {
case "udp4":
return ice.NetworkTypeUDP4, nil
case "udp6":
return ice.NetworkTypeUDP6, nil
case "tcp4":
return ice.NetworkTypeTCP4, nil
case "tcp6":
return ice.NetworkTypeTCP6, nil
default:
return ice.NetworkTypeTCP4, fmt.Errorf("unknown network type: %s", t)
}
}
func Parse(progname string, argv []string) (*Args, error) {
var uri string
var err error
var iceURLs, iceCandidateTypes, iceNetworkTypes, iceNat1to1IPs arrayFlags
flags := flag.NewFlagSet(progname, flag.ContinueOnError)
flags.Usage = showUsage
logLevel := flags.String("log-level", "info", "log level (one of \"panic\", \"fatal\", \"error\", \"warn\", \"info\", \"debug\", \"trace\")")
// discover := flag.Bool("discover", false, "discover peers using the backend")
backend := flags.String("backend", "http://localhost:8080", "backend URL")
backendOpts := flags.String("backend-opts", "", "comma-separated list of additional backend options (e.g. \"key1=val1,key2-val2\")")
user := flags.Bool("user", false, "start userspace Wireguard daemon")
proxyType := flags.String("proxy", "auto", "proxy type to use")
interfaceFilter := flags.String("interface-filter", ".*", "regex for filtering Wireguard interfaces (e.g. \"wg-.*\")")
configSync := flags.Bool("config-sync", false, "sync Wireguard interface with configuration file (see \"wg synconf\"")
configPath := flags.String("config-path", "/etc/wireguard", "base path to search for Wireguard configuration files")
watchInterval := flags.Duration("watch-interval", 2*time.Second, "interval at which we are polling the kernel for updates on the Wireguard interfaces")
// ice.AgentConfig fields
flags.Var(&iceURLs, "url", "STUN and/or TURN server address (**)")
flags.Var(&iceCandidateTypes, "ice-candidate-type", "usable candidate types (**, one of \"host\", \"srflx\", \"prflx\", \"relay\")")
flags.Var(&iceNetworkTypes, "ice-network-type", "usable network types (**, select from \"udp4\", \"udp6\", \"tcp4\", \"tcp6\")")
flags.Var(&iceNat1to1IPs, "ice-nat-1to1-ips", "list of IP addresses which will be added as local server reflexive candidates (**)")
icePortMin := flags.Uint("ice-port-min", 0, "minimum port for allocation policy (range: 0-65535)")
icePortMax := flags.Uint("ice-port-max", 0, "maximum port for allocation policy (range: 0-65535)")
iceLite := flags.Bool("ice-lite", false, "lite agents do not perform connectivity check and only provide host candidates")
iceMdns := flags.Bool("ice-mdns", false, "enable local Multicast DNS discovery")
iceMaxBindingRequests := flags.Int("ice-max-binding-requests", defaultMaxBindingRequests, "maximum number of binding request before considering a pair failed")
iceInsecureSkipVerify := flags.Bool("ice-insecure-skip-verify", false, "skip verification of TLS certificates for secure STUN/TURN servers")
iceInterfaceFilter := flags.String("ice-interface-filter", ".*", "regex for filtering local interfaces for ICE candidate gathering (e.g. \"eth[0-9]+\")")
iceDisconnectedTimeout := flags.Duration("ice-disconnected-timout", defaultDisconnectedTimeout, "time till an Agent transitions disconnected")
iceFailedTimeout := flags.Duration("ice-failed-timeout", defaultFailedTimeout, "time until an Agent transitions to failed after disconnected")
iceKeepaliveInterval := flags.Duration("ice-keepalive-interval", defaultKeepaliveInterval, "interval netween STUN keepalives")
iceCheckInterval := flags.Duration("ice-check-interval", defaultCheckInterval, "interval at which the agent performs candidate checks in the connecting phase")
iceRestartInterval := flags.Duration("ice-restart-interval", defaultDisconnectedTimeout, "time to wait before ICE restart")
iceUsername := flags.String("ice-user", "", "username for STUN/TURN credentials")
icePassword := flags.String("ice-pass", "", "password for STUN/TURN credentials")
// iceMaxBindingRequestTimeout := flag.Duration("ice-max-binding-request-timeout", maxBindingRequestTimeout, "wait time before binding requests can be deleted")
flags.Parse(argv)
args := &Args{
User: *user,
ProxyType: proxy.ProxyTypeFromString(*proxyType),
BackendOptions: make(map[string]string),
// Discover: *discover,
ConfigSync: *configSync,
ConfigPath: *configPath,
WatchInterval: *watchInterval,
RestartInterval: *iceRestartInterval,
Interfaces: flag.Args(),
AgentConfig: ice.AgentConfig{
PortMin: uint16(*icePortMin),
PortMax: uint16(*icePortMax),
Lite: *iceLite,
InsecureSkipVerify: *iceInsecureSkipVerify,
},
}
// Find best proxy method
if args.ProxyType == proxy.ProxyTypeAuto {
args.ProxyType = proxy.AutoProxy()
}
// Check proxy type
if args.ProxyType == proxy.ProxyTypeInvalid {
return nil, fmt.Errorf("invalid proxy type: %s", *proxyType)
}
// Compile interface regex
args.InterfaceRegex, err = regexp.Compile(*interfaceFilter)
if err != nil {
return nil, fmt.Errorf("invalid interface filter: %w", err)
}
// Parse log level
if lvl, err := log.ParseLevel(*logLevel); err != nil {
return nil, fmt.Errorf("invalid log level: %s", *logLevel)
} else {
log.SetLevel(lvl)
}
// Parse backend URI
if !strings.Contains(*backend, ":") {
*backend += ":"
}
if args.Backend, err = url.Parse(*backend); err != nil {
return nil, fmt.Errorf("invalid URI: %w", err)
}
// Parse additional backend options
if *backendOpts != "" {
opts := strings.Split(*backendOpts, ",")
for _, opt := range opts {
kv := strings.SplitN(opt, "=", 2)
if len(kv) < 2 {
return nil, fmt.Errorf("invalid backend option: %s", opt)
}
key := kv[0]
value := kv[1]
args.BackendOptions[key] = value
}
}
if *iceMaxBindingRequests >= 0 {
maxBindingReqs := uint16(*iceMaxBindingRequests)
args.AgentConfig.MaxBindingRequests = &maxBindingReqs
}
if *iceMdns {
args.AgentConfig.MulticastDNSMode = ice.MulticastDNSModeQueryAndGather
}
if *iceDisconnectedTimeout > 0 {
args.AgentConfig.DisconnectedTimeout = iceDisconnectedTimeout
}
if *iceFailedTimeout > 0 {
args.AgentConfig.FailedTimeout = iceFailedTimeout
}
if *iceKeepaliveInterval > 0 {
args.AgentConfig.KeepaliveInterval = iceKeepaliveInterval
}
if *iceCheckInterval > 0 {
args.AgentConfig.CheckInterval = iceCheckInterval
}
if len(iceNat1to1IPs) > 0 {
args.AgentConfig.NAT1To1IPCandidateType = ice.CandidateTypeServerReflexive
args.AgentConfig.NAT1To1IPs = iceNat1to1IPs
}
args.IceInterfaceRegex, err = regexp.Compile(*iceInterfaceFilter)
if err != nil {
return nil, fmt.Errorf("failed to compile interface regex: %w", err)
}
// Parse candidate types
for _, c := range iceCandidateTypes {
candType, err := candidateTypeFromString(c)
if err != nil {
return nil, err
}
args.AgentConfig.CandidateTypes = append(args.AgentConfig.CandidateTypes, candType)
}
// Parse network types
if len(iceNetworkTypes) == 0 {
args.AgentConfig.NetworkTypes = []ice.NetworkType{
ice.NetworkTypeUDP4,
ice.NetworkTypeUDP6,
}
} else {
for _, n := range iceNetworkTypes {
netType, err := networkTypeFromString(n)
if err != nil {
return nil, err
}
args.AgentConfig.NetworkTypes = append(args.AgentConfig.NetworkTypes, netType)
}
}
// Parse ICE urls
for _, uri = range iceURLs {
iceUrl, err := ice.ParseURL(uri)
if err != nil {
return nil, fmt.Errorf("failed to parse url %s: %w", uri, err)
}
if *iceUsername != "" {
iceUrl.Username = *iceUsername
}
if *icePassword != "" {
iceUrl.Password = *icePassword
}
args.AgentConfig.Urls = append(args.AgentConfig.Urls, iceUrl)
}
// Add default STUN server
if len(args.AgentConfig.Urls) == 0 {
url := &ice.URL{
Scheme: ice.SchemeTypeSTUN,
Host: "stun.l.google.com",
Port: 19302,
Username: "",
Password: "",
Proto: ice.ProtoTypeUDP,
}
args.AgentConfig.Urls = append(args.AgentConfig.Urls, url)
}
args.AgentConfig.LoggerFactory = &pice.LoggerFactory{}
return args, nil
}

113
pkg/args/args_test.go Normal file
View File

@@ -0,0 +1,113 @@
package args_test
import (
"testing"
"github.com/pion/ice/v2"
"riasc.eu/wice/pkg/args"
)
func TestParseArgsUser(t *testing.T) {
config, err := args.Parse("prog", []string{"-user"})
if err != nil {
t.Errorf("err got %v, want nil", err)
}
if !config.User {
t.Fail()
}
}
func TestParseArgsBackend(t *testing.T) {
config, err := args.Parse("prog", []string{"-backend", "k8s", "-backend-opts", "key=value"})
if err != nil {
t.Errorf("err got %v, want nil", err)
}
if config.Backend.Scheme != "k8s" {
t.Fail()
}
if config.BackendOptions["key"] != "value" {
t.Fail()
}
}
func TestParseArgsUrls(t *testing.T) {
config, err := args.Parse("prog", []string{"-url", "stun:stun.riasc.eu", "-url", "turn:turn.riasc.eu"})
if err != nil {
t.Errorf("err got %v, want nil", err)
}
if len(config.AgentConfig.Urls) != 2 {
t.Fail()
}
if config.AgentConfig.Urls[0].Host != "stun.riasc.eu" {
t.Fail()
}
if config.AgentConfig.Urls[0].Scheme != ice.SchemeTypeSTUN {
t.Fail()
}
if config.AgentConfig.Urls[1].Host != "turn.riasc.eu" {
t.Fail()
}
if config.AgentConfig.Urls[1].Scheme != ice.SchemeTypeTURN {
t.Fail()
}
}
func TestParseArgsCandidateTypes(t *testing.T) {
config, err := args.Parse("prog", []string{"-ice-candidate-type", "host", "-ice-candidate-type", "relay"})
if err != nil {
t.Errorf("err got %v, want nil", err)
}
if len(config.AgentConfig.CandidateTypes) != 2 {
t.Fail()
}
if config.AgentConfig.CandidateTypes[0] != ice.CandidateTypeHost {
t.Fail()
}
if config.AgentConfig.CandidateTypes[1] != ice.CandidateTypeRelay {
t.Fail()
}
}
func TestParseArgsInterfaceFilter(t *testing.T) {
config, err := args.Parse("prog", []string{"-interface-filter", "eth\\d+"})
if err != nil {
t.Errorf("err got %v, want nil", err)
}
if !config.InterfaceRegex.Match([]byte("eth0")) {
t.Fail()
}
if config.InterfaceRegex.Match([]byte("wifi0")) {
t.Fail()
}
}
func TestParseArgsInterfaceFilterFail(t *testing.T) {
_, err := args.Parse("prog", []string{"-interface-filter", "eth("})
if err == nil {
t.Fail()
}
}
func TestParseArgsDefault(t *testing.T) {
config, err := args.Parse("prog", []string{})
if err != nil {
t.Fail()
}
if len(config.AgentConfig.Urls) != 1 {
t.Fail()
}
}

37
pkg/backend/backend.go Normal file
View File

@@ -0,0 +1,37 @@
package backend
import (
"fmt"
"io"
"net/url"
"riasc.eu/wice/pkg/crypto"
)
var (
Backends = map[BackendType]*BackendPlugin{}
)
type Backend interface {
PublishOffer(kp crypto.PublicKeyPair, offer Offer) error
SubscribeOffer(kp crypto.PublicKeyPair) (chan Offer, error)
WithdrawOffer(kp crypto.PublicKeyPair) error
io.Closer
}
func NewBackend(uri *url.URL, options map[string]string) (Backend, error) {
typ := BackendType(uri.Scheme)
p, ok := Backends[typ]
if !ok {
return nil, fmt.Errorf("unknown backend type: %s", typ)
}
be, err := p.New(uri, options)
if err != nil {
return nil, fmt.Errorf("failed to create backend: %w", err)
}
return be, nil
}

View File

@@ -0,0 +1,26 @@
package backend_test
import (
"net/url"
"testing"
"riasc.eu/wice/pkg/backend"
"riasc.eu/wice/pkg/backend/http"
_ "riasc.eu/wice/pkg/backend/http"
)
func TestNewBackend(t *testing.T) {
uri, err := url.Parse("http://example.com")
if err != nil {
t.Fail()
}
b, err := backend.NewBackend(uri, map[string]string{})
if err != nil {
t.Fail()
}
if _, ok := b.(*http.Backend); !ok {
t.Fail()
}
}

View File

@@ -0,0 +1,58 @@
package base
import (
"net/url"
log "github.com/sirupsen/logrus"
"riasc.eu/wice/pkg/backend"
"riasc.eu/wice/pkg/crypto"
)
type Backend struct {
Offers map[crypto.PublicKeyPair]chan backend.Offer
Logger log.FieldLogger
Type string
}
func NewBackend(uri *url.URL, options map[string]string) Backend {
logFields := log.Fields{
"logger": "backend",
"backend": uri.Scheme,
}
b := Backend{
Offers: make(map[crypto.PublicKeyPair]chan backend.Offer),
Logger: log.WithFields(logFields),
}
return b
}
func (b *Backend) Close() error {
return nil
}
func (b *Backend) SubscribeOffers(kp crypto.PublicKeyPair) chan backend.Offer {
b.Logger.WithField("kp", kp).Info("Subscribe to offers from peer")
ch, ok := b.Offers[kp]
if !ok {
ch = make(chan backend.Offer, 100)
b.Offers[kp] = ch
}
return ch
}
func (b *Backend) PublishOffer(kp crypto.PublicKeyPair, offer backend.Offer) error {
b.Logger.WithField("kp", kp).WithField("offer", offer).Debug("Published offer")
return nil
}
func (b *Backend) WithdrawOffer(kp crypto.PublicKeyPair) error {
b.Logger.WithField("kp", kp).Debug("Withdrawed offer")
return nil
}

View File

@@ -0,0 +1,13 @@
package base
import "net/url"
type BackendConfig struct {
URI *url.URL
}
func (c *BackendConfig) Parse(uri *url.URL, options map[string]string) error {
c.URI = uri
return nil
}

166
pkg/backend/http/backend.go Normal file
View File

@@ -0,0 +1,166 @@
package http
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
"riasc.eu/wice/pkg/backend"
"riasc.eu/wice/pkg/backend/base"
"riasc.eu/wice/pkg/crypto"
)
type Backend struct {
base.Backend
config BackendConfig
client *http.Client
}
func init() {
p := backend.BackendPlugin{
New: NewBackend,
Description: "Simple HTTP/HTTPs REST API server",
}
backend.Backends["http"] = &p
backend.Backends["https"] = &p
}
func NewBackend(uri *url.URL, options map[string]string) (backend.Backend, error) {
b := &Backend{
Backend: base.NewBackend(uri, options),
}
b.config.Parse(uri, options)
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: b.config.InsecureSkipVerify,
},
}
b.client = &http.Client{
Transport: tr,
Timeout: b.config.Timeout,
}
go b.pollOffers()
return b, nil
}
func (b *Backend) SubscribeOffer(kp crypto.PublicKeyPair) (chan backend.Offer, error) {
ch := b.Backend.SubscribeOffers(kp)
// Get initial offer without waiting for poller
o, err := b.getOffer(kp)
if err != nil {
return nil, fmt.Errorf("failed to get offer: %w", err)
}
if o.ID != 0 {
ch <- o
}
return ch, nil
}
// pollOffers periodically fetches offers from the HTTP API and feeds them into the subscribption channels
func (b *Backend) pollOffers() {
b.Logger.Info("Start polling for new offers")
ticker := time.NewTicker(b.config.PollInterval)
for range ticker.C {
for kp, ch := range b.Offers {
o, err := b.getOffer(kp)
if err != nil {
b.Logger.WithError(err).Error("Failed to fetch offer")
continue
}
if o.ID != 0 {
ch <- o
}
}
}
}
// PublishOffer POSTs the Offer to the HTTP API
func (b *Backend) PublishOffer(kp crypto.PublicKeyPair, offer backend.Offer) error {
buf, err := json.Marshal(offer)
if err != nil {
return fmt.Errorf("failed to encode offer: %w", err)
}
resp, err := b.client.Post(b.offerUrl(kp, false), "application/json", bytes.NewBuffer(buf))
if err != nil {
return fmt.Errorf("failed HTTP request: %w", err)
} else if resp.StatusCode != http.StatusOK {
return fmt.Errorf("failed HTTP request: %s", resp.Status)
}
return b.Backend.PublishOffer(kp, offer)
}
func (b *Backend) getOffer(kp crypto.PublicKeyPair) (backend.Offer, error) {
b.Logger.WithField("kp", kp).Trace("Fetching offer")
resp, err := b.client.Get(b.offerUrl(kp, true))
if err != nil {
return backend.Offer{}, fmt.Errorf("failed HTTP request: %w", err)
} else if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusNotFound {
return backend.Offer{}, nil
} else {
return backend.Offer{}, fmt.Errorf("failed HTTP request: %s", resp.Status)
}
}
var offer backend.Offer
dec := json.NewDecoder(resp.Body)
err = dec.Decode(&offer)
if err != nil {
return backend.Offer{}, err
}
b.Logger.WithField("offer", offer).Debug("Fetched offer")
return offer, nil
}
func (b *Backend) WithdrawOffer(kp crypto.PublicKeyPair) error {
req, err := http.NewRequest(http.MethodDelete, b.offerUrl(kp, false), nil)
if err != nil {
return err
}
resp, err := b.client.Do(req)
if err != nil {
return fmt.Errorf("failed HTTP request: %w", err)
} else if resp.StatusCode != http.StatusOK {
return fmt.Errorf("failed HTTP request: %s", resp.Status)
}
return b.Backend.WithdrawOffer(kp)
}
func (b *Backend) Close() error {
b.client.CloseIdleConnections()
return nil // TODO
}
func (b *Backend) offerUrl(kp crypto.PublicKeyPair, sub bool) string {
u := *b.config.URI
if sub {
u.Path += "/offers/" + url.PathEscape(kp.Theirs.String()) + "/" + url.PathEscape(kp.Ours.String())
} else {
u.Path += "/offers/" + url.PathEscape(kp.Ours.String()) + "/" + url.PathEscape(kp.Theirs.String())
}
return u.String()
}

View File

@@ -0,0 +1,53 @@
package http
import (
"fmt"
"net/url"
"time"
"riasc.eu/wice/pkg/backend/base"
)
const (
defaultPollInterval = 5 * time.Second
defaultTimeout = 10 * time.Second
)
type BackendConfig struct {
base.BackendConfig
PollInterval time.Duration
Timeout time.Duration
InsecureSkipVerify bool
}
func (c *BackendConfig) Parse(uri *url.URL, options map[string]string) error {
err := c.BackendConfig.Parse(uri, options)
if err != nil {
return err
}
if skip, ok := options["insecure_skip_verify"]; ok {
c.InsecureSkipVerify = skip == "true"
}
if interval, ok := options["interval"]; ok {
c.PollInterval, err = time.ParseDuration(interval)
if err != nil {
return fmt.Errorf("invalid interval: %s", interval)
}
} else {
c.PollInterval = defaultPollInterval
}
if timeout, ok := options["timeout"]; ok {
c.Timeout, err = time.ParseDuration(timeout)
if err != nil {
return fmt.Errorf("invalid timeout: %s", timeout)
}
} else {
c.Timeout = defaultTimeout
}
return nil
}

190
pkg/backend/k8s/backend.go Normal file
View File

@@ -0,0 +1,190 @@
package k8s
import (
"context"
"encoding/json"
"fmt"
"net/url"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/clientcmd"
"riasc.eu/wice/pkg/backend"
"riasc.eu/wice/pkg/backend/base"
"riasc.eu/wice/pkg/crypto"
)
const (
annotationPrefix string = "wice.riasc.eu"
defaultAnnotationOffers string = annotationPrefix + "/offers"
defaultAnnotationPublicKey string = annotationPrefix + "/public-key"
)
type Backend struct {
base.Backend
config BackendConfig
clientSet *kubernetes.Clientset
informer cache.SharedInformer
term chan struct{}
updates chan NodeCallback
}
func init() {
backend.Backends["k8s"] = &backend.BackendPlugin{
New: NewBackend,
Description: "Exchange candidates via annotation in Kubernetes Node resource",
}
}
func NewBackend(uri *url.URL, options map[string]string) (backend.Backend, error) {
b := Backend{
Backend: base.NewBackend(uri, options),
term: make(chan struct{}),
updates: make(chan NodeCallback),
}
err := b.config.Parse(uri, options)
if err != nil {
return nil, fmt.Errorf("failed to parse configuration: %w", err)
}
kubeconfig := uri.Path
var config *rest.Config
if kubeconfig == "" {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
// if you want to change the loading rules (which files in which order), you can do so here
configOverrides := &clientcmd.ConfigOverrides{}
// if you want to change override values or bind them to flags, there are methods to help you
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
config, err = kubeConfig.ClientConfig()
if err != nil {
return nil, fmt.Errorf("failed to load config: %w", err)
}
} else if kubeconfig == "incluster" {
config, err = rest.InClusterConfig()
if err != nil {
return nil, fmt.Errorf("failed to get incluster configuration: %w", err)
}
} else {
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
return nil, fmt.Errorf("failed to get configuration from flags: %w", err)
}
}
// Create the clientset
b.clientSet, err = kubernetes.NewForConfig(config)
if err != nil {
return nil, fmt.Errorf("failed to create clientset: %w", err)
}
// Create the shared informer factory and use the client to connect to
// Kubernetes
factory := informers.NewSharedInformerFactoryWithOptions(b.clientSet, 0,
informers.WithTweakListOptions(func(options *metav1.ListOptions) {
// options.LabelSelector = b.config.AnnotationPublicKey
}))
// Get the informer for the right resource, in this case a Pod
b.informer = factory.Core().V1().Nodes().Informer()
b.informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: b.onNodeAdd,
UpdateFunc: b.onNodeUpdate,
DeleteFunc: b.onNodeDelete,
})
go b.informer.Run(b.term)
b.Logger.Debug("Started watching node resources")
go b.applyUpdates()
b.Logger.Debug("Started batched updates")
return &b, nil
}
func (b *Backend) SubscribeOffer(kp crypto.PublicKeyPair) (chan backend.Offer, error) {
ch := b.Backend.SubscribeOffers(kp)
// Process the node annotation at least once before we rely on the informer
node, err := b.getNodeByPublicKey(kp.Theirs)
if err == nil {
b.processNode(node)
}
return ch, nil
}
func (b *Backend) PublishOffer(kp crypto.PublicKeyPair, offer backend.Offer) error {
b.updateNode(func(node *corev1.Node) error {
offerMapJson, ok := node.ObjectMeta.Annotations[b.config.AnnotationOffers]
// Unmarshal
var om backend.OfferMap
if ok && offerMapJson != "" {
err := json.Unmarshal([]byte(offerMapJson), &om)
if err != nil {
return err
}
} else {
om = backend.OfferMap{}
}
// Update
om[kp.Theirs] = offer
// Marshal
offerMapJsonNew, err := json.Marshal(&om)
if err != nil {
return err
}
node.ObjectMeta.Annotations[b.config.AnnotationOffers] = string(offerMapJsonNew)
node.ObjectMeta.Annotations[b.config.AnnotationPublicKey] = kp.Ours.String()
return nil
})
return b.Backend.PublishOffer(kp, offer)
}
func (b *Backend) WithdrawOffer(kp crypto.PublicKeyPair) error {
b.updateNode(func(node *corev1.Node) error {
delete(node.ObjectMeta.Annotations, b.config.AnnotationOffers)
return nil
})
return b.Backend.WithdrawOffer(kp)
}
func (b *Backend) Close() error {
close(b.term)
return nil // TODO
}
func (b *Backend) getNodeByPublicKey(pk crypto.Key) (*corev1.Node, error) {
coreV1 := b.clientSet.CoreV1()
nodes, err := coreV1.Nodes().List(context.TODO(), metav1.ListOptions{
LabelSelector: fmt.Sprintf("%s=%s", b.config.AnnotationPublicKey, pk),
})
if err != nil {
return nil, err
}
if len(nodes.Items) != 1 {
return nil, fmt.Errorf("could not find node with public key: %s", pk)
}
return &nodes.Items[0], nil
}

View File

@@ -0,0 +1,47 @@
package k8s_test
import (
"log"
"net/url"
"testing"
"riasc.eu/wice/pkg/backend"
"riasc.eu/wice/pkg/backend/k8s"
"riasc.eu/wice/pkg/crypto"
)
func TestBackend(t *testing.T) {
opts := map[string]string{
"nodename": "red",
}
uri, err := url.Parse("k8s://")
if err != nil {
t.Errorf("failed to parse backend URL: %w", err)
}
b, err := k8s.NewBackend(uri, opts)
if err != nil {
t.Errorf("failed to create backend: %w", err)
}
ourSecretKey, _ := crypto.GeneratePrivateKey()
theirSecretKey, _ := crypto.GeneratePrivateKey()
kp := crypto.PublicKeyPair{
Ours: ourSecretKey.PublicKey(),
Theirs: theirSecretKey.PublicKey(),
}
o := backend.NewOffer()
ch, err := b.SubscribeOffer(kp)
if err != nil {
t.Errorf("failed to subscribe to offer")
}
b.PublishOffer(kp, o)
n := <-ch
log.Print(n)
}

42
pkg/backend/k8s/config.go Normal file
View File

@@ -0,0 +1,42 @@
package k8s
import (
"errors"
"net/url"
"riasc.eu/wice/pkg/backend/base"
)
type BackendConfig struct {
base.BackendConfig
NodeName string
AnnotationOffers string
AnnotationPublicKey string
}
func (c *BackendConfig) Parse(uri *url.URL, options map[string]string) error {
var ok bool
err := c.BackendConfig.Parse(uri, options)
if err != nil {
return err
}
c.NodeName, ok = options["nodename"]
if !ok {
return errors.New("missing backend option: nodename")
}
c.AnnotationOffers, ok = options["annotation-offers"]
if !ok {
c.AnnotationOffers = defaultAnnotationOffers
}
c.AnnotationPublicKey, ok = options["annotation-public-key"]
if !ok {
c.AnnotationPublicKey = defaultAnnotationPublicKey
}
return nil
}

View File

@@ -0,0 +1,81 @@
package k8s
import (
"encoding/json"
corev1 "k8s.io/api/core/v1"
"riasc.eu/wice/pkg/backend"
"riasc.eu/wice/pkg/crypto"
)
func (b *Backend) onNodeAdd(obj interface{}) {
node := obj.(*corev1.Node)
b.Logger.WithField("name", node.ObjectMeta.Name).Debug("Node added")
b.processNode(node)
}
func (b *Backend) onNodeUpdate(_ interface{}, new interface{}) {
newNode := new.(*corev1.Node)
b.Logger.WithField("name", newNode.ObjectMeta.Name).Debug("Node updated")
b.processNode(newNode)
}
func (b *Backend) onNodeDelete(obj interface{}) {
node := obj.(*corev1.Node)
b.Logger.WithField("name", node.ObjectMeta.Name).Debug("Node deleted")
b.processNode(node)
}
func (b *Backend) processNode(node *corev1.Node) {
// Ignore ourself
if node.ObjectMeta.Name == b.config.NodeName {
b.Logger.WithField("node", node.ObjectMeta.Name).Trace("Ignoring ourself")
return
}
// Check if required annotations are present
offersJson, ok := node.ObjectMeta.Annotations[b.config.AnnotationOffers]
if !ok {
b.Logger.WithField("node", node.ObjectMeta.Name).Trace("Missing candidate annotation")
return
}
keyString, ok := node.ObjectMeta.Annotations[b.config.AnnotationPublicKey]
if !ok {
b.Logger.WithField("node", node.ObjectMeta.Name).Trace("Missing public key annotation")
return
}
var err error
var theirPK crypto.Key
theirPK, err = crypto.ParseKey(keyString)
if err != nil {
b.Logger.WithError(err).Warn("Failed to parse public key")
}
var om backend.OfferMap
err = json.Unmarshal([]byte(offersJson), &om)
if err != nil {
b.Logger.WithError(err).Warn("Failed to parse candidate annotation")
return
}
for ourPK, o := range om {
kp := crypto.PublicKeyPair{
Ours: ourPK,
Theirs: theirPK,
}
ch, ok := b.Offers[kp]
if !ok {
b.Logger.WithField("kp", kp).Warn("Found candidates for unknown peer")
continue
}
ch <- o
}
}

62
pkg/backend/k8s/update.go Normal file
View File

@@ -0,0 +1,62 @@
package k8s
import (
"context"
"fmt"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/util/retry"
)
type NodeCallback func(*corev1.Node) error
type AnnotationCallback func(string) (string, error)
type AnnotationCallbackMap map[string]AnnotationCallback
func (b *Backend) applyUpdates() {
var cbs []NodeCallback
var timer time.Timer
for {
select {
case cb := <-b.updates:
cbs = append(cbs, cb)
timer = *time.NewTimer(50 * time.Millisecond)
case <-timer.C:
if len(cbs) > 0 {
b.Logger.Debugf("Applying %d batched updates", len(cbs))
nodes := b.clientSet.CoreV1().Nodes()
err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
node, err := nodes.Get(context.TODO(), b.config.NodeName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get latest version of node %s: %w", b.config.NodeName, err)
}
for _, cb := range cbs {
if err := cb(node); err != nil {
return err
}
}
_, err = nodes.Update(context.TODO(), node, metav1.UpdateOptions{})
return err
})
if err != nil {
b.Logger.WithError(err).Error("Failed to update node")
}
cbs = nil
}
}
}
}
func (b *Backend) updateNode(cb NodeCallback) {
b.updates <- cb
}

163
pkg/backend/p2p/backend.go Normal file
View File

@@ -0,0 +1,163 @@
package p2p
import (
"context"
"fmt"
"net/url"
"sync"
"github.com/ipfs/go-cid"
libp2p "github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p-core/host"
"github.com/libp2p/go-libp2p-core/peer"
dht "github.com/libp2p/go-libp2p-kad-dht"
multiaddr "github.com/multiformats/go-multiaddr"
"github.com/multiformats/go-multihash"
"riasc.eu/wice/pkg/backend"
"riasc.eu/wice/pkg/backend/base"
"riasc.eu/wice/pkg/crypto"
log "github.com/sirupsen/logrus"
)
const (
ProtocolID = "/wice/rpc/0.1.0"
// See: https://github.com/multiformats/multicodec/blob/master/table.csv#L85
CodeX25519PublicKey = 0xec
)
func init() {
backend.Backends["p2p"] = &backend.BackendPlugin{
New: NewBackend,
Description: "LibP2P Kademlia DHT",
}
}
type Backend struct {
base.Backend
config BackendConfig
host host.Host
peers PeerList
context context.Context
dht *dht.IpfsDHT
}
func NewBackend(uri *url.URL, options map[string]string) (backend.Backend, error) {
var err error
b := Backend{
Backend: base.NewBackend(uri, options),
}
b.config.Parse(uri, options)
b.context = context.Background()
b.host, err = libp2p.New(b.context, libp2p.ListenAddrs([]multiaddr.Multiaddr(b.config.ListenAddresses)...))
if err != nil {
return nil, fmt.Errorf("failed to create host: %w", err)
}
b.Logger.WithField("id", b.host.ID()).WithField("addrs", b.host.Addrs()).Info("Host created")
b.host.SetStreamHandler(ProtocolID, b.handleStream)
b.dht, err = dht.New(b.context, b.host)
if err != nil {
return nil, fmt.Errorf("failed to create DHT: %w", err)
}
b.Logger.Debug("Bootstrapping the DHT")
if err = b.dht.Bootstrap(b.context); err != nil {
return nil, fmt.Errorf("failed to bootstrap DHT: %w", err)
}
// Let's connect to the bootstrap nodes first. They will tell us about the
// other nodes in the network.
var wg sync.WaitGroup
for _, peerAddr := range b.config.BootstrapPeers {
peerinfo, _ := peer.AddrInfoFromP2pAddr(peerAddr)
wg.Add(1)
go func() {
defer wg.Done()
if err := b.host.Connect(b.context, *peerinfo); err != nil {
log.Warning(err)
} else {
b.Logger.WithField("peer", *peerinfo).Info("Connection established with bootstrap node")
}
}()
}
wg.Wait()
// // We use a rendezvous point "meet me here" to announce our location.
// // This is like telling your friends to meet you at the Eiffel Tower.
// b.Logger.Info("Announcing ourselves...")
// routingDiscovery := discovery.NewRoutingDiscovery(b.dht)
// discovery.Advertise(b.context, routingDiscovery, b.config.RendezvousString)
// b.Logger.Debug("Successfully announced!")
// // Now, look for others who have announced
// // This is like your friend telling you the location to meet you.
// b.Logger.Debug("Searching for other peers...")
// peerChan, err := routingDiscovery.FindPeers(b.context, b.config.RendezvousString)
// if err != nil {
// return nil, fmt.Errorf("failed to find peers: %w", err)
// }
// go b.handlePeers(peerChan)
return &b, nil
}
func (b *Backend) SubscribeOffer(kp crypto.PublicKeyPair) (chan backend.Offer, error) {
ch := b.Backend.SubscribeOffers(kp)
cid := cidForPublicKey(kp.Ours)
err := b.dht.Provide(b.context, cid, true)
if err != nil {
return nil, err
}
return ch, nil
}
func (b *Backend) PublishOffer(kp crypto.PublicKeyPair, offer backend.Offer) error {
cid := cidForPublicKey(kp.Theirs)
peerChan := b.dht.FindProvidersAsync(b.context, cid, 0)
go func() {
for pai := range peerChan {
peer, err := NewPeer(b, pai.ID)
if err != nil {
b.Logger.WithError(err).Error("Failed to create peer")
return
}
om := backend.OfferMap{
kp.Ours: offer,
}
var ret bool
err = peer.Client.Call("candidates.Publish", &om, &ret)
if err != nil {
b.Logger.WithError(err).Error("Failed RPC call")
}
b.peers = append(b.peers, peer)
}
}()
return b.PublishOffer(kp, offer)
}
func (b *Backend) Close() error {
return nil // TODO
}
func cidForPublicKey(pk crypto.Key) cid.Cid {
mh, _ := multihash.Encode(pk[:], multihash.IDENTITY)
return cid.NewCidV1(CodeX25519PublicKey, mh)
}

64
pkg/backend/p2p/config.go Normal file
View File

@@ -0,0 +1,64 @@
package p2p
import (
"fmt"
"net/url"
"strings"
"riasc.eu/wice/pkg/backend/base"
dht "github.com/libp2p/go-libp2p-kad-dht"
maddr "github.com/multiformats/go-multiaddr"
)
type addressList []maddr.Multiaddr
type BackendConfig struct {
base.BackendConfig
// Load some options
ListenAddresses addressList
BootstrapPeers addressList
RendezvousString string
}
func (al addressList) Set(option string) error {
as := strings.Split(option, ":")
for _, a := range as {
ma, err := maddr.NewMultiaddr(a)
if err != nil {
return err
}
al = append(al, ma)
}
return nil
}
func (c *BackendConfig) Parse(uri *url.URL, options map[string]string) error {
if rStr, ok := options["rendevouz-string"]; ok {
c.RendezvousString = rStr
} else {
c.RendezvousString = uri.Host
}
if laStr, ok := options["listen-addresses"]; ok {
err := c.ListenAddresses.Set(laStr)
if err != nil {
return fmt.Errorf("failed to parse listen-address option: %w", err)
}
}
if bpStr, ok := options["bootstrap-peers"]; ok {
if err := c.BootstrapPeers.Set(bpStr); err != nil {
return fmt.Errorf("failed to parse listen-address option: %w", err)
}
}
if len(c.BootstrapPeers) == 0 {
c.BootstrapPeers = dht.DefaultBootstrapPeers
}
return nil
}

View File

@@ -0,0 +1,42 @@
package p2p
import (
"github.com/libp2p/go-libp2p-core/network"
"github.com/libp2p/go-libp2p-core/peer"
)
func (b *Backend) handleStream(stream network.Stream) {
var err error
peerID := stream.Conn().RemotePeer()
peer := b.peers.GetByPeerId(peerID)
if peer == nil {
peer, err = NewPeer(b, peerID)
if err != nil {
b.Logger.WithError(err).Fatal("Failed to create peer")
return
}
}
peer.HandleStream(stream)
}
func (b *Backend) handlePeers(peerChan <-chan peer.AddrInfo) {
var err error
for peer := range peerChan {
if peer.ID == b.host.ID() {
continue
}
p := b.peers.GetByPeerId(peer.ID)
if p == nil {
p, err = NewPeer(b, peer.ID)
if err != nil {
b.Logger.WithError(err).Error("Failed to create peer")
}
b.peers = append(b.peers, p)
}
}
}

85
pkg/backend/p2p/peer.go Normal file
View File

@@ -0,0 +1,85 @@
package p2p
import (
"fmt"
"net/rpc"
"github.com/libp2p/go-libp2p-core/network"
"github.com/libp2p/go-libp2p-core/peer"
log "github.com/sirupsen/logrus"
"riasc.eu/wice/pkg/crypto"
)
type Peer struct {
ID peer.ID
Stream network.Stream
Backend *Backend
PublicKey crypto.Key
Client *rpc.Client
logger *log.Entry
}
type PeerList []*Peer
func (pl *PeerList) GetByPeerId(id peer.ID) *Peer {
for _, peer := range *pl {
if peer.ID == id {
return peer
}
}
return nil
}
func (pl *PeerList) GetByPublicKey(pk crypto.Key) *Peer {
for _, peer := range *pl {
if peer.PublicKey == pk {
return peer
}
}
return nil
}
func NewPeer(b *Backend, id peer.ID) (*Peer, error) {
var err error
p := Peer{
ID: id,
Backend: b,
logger: b.Logger.WithField("peer", id),
}
p.logger.Debug("Connecting to peer")
p.Stream, err = b.host.NewStream(b.context, p.ID, ProtocolID)
if err != nil {
return nil, fmt.Errorf("failed to connect to peer: %w", err)
}
p.logger.Info("Connected to peer")
p.Client = rpc.NewClient(p.Stream)
return &p, nil
}
func (p *Peer) Close() error {
if p.Stream != nil {
return p.Stream.Close()
}
return nil
}
func (p *Peer) HandleStream(stream network.Stream) {
server := rpc.NewServer()
server.RegisterName("candidates", NewCandidateService(p))
server.RegisterName("peers", NewPeerService(p))
go server.ServeConn(stream)
}

55
pkg/backend/p2p/rpc.go Normal file
View File

@@ -0,0 +1,55 @@
package p2p
import (
"riasc.eu/wice/pkg/backend"
"riasc.eu/wice/pkg/crypto"
)
type CandidateService struct {
Peer *Peer
}
func NewCandidateService(p *Peer) *CandidateService {
return &CandidateService{
Peer: p,
}
}
func (svc *CandidateService) Publish(om backend.OfferMap, result *bool) error {
for pk, o := range om {
kp := crypto.PublicKeyPair{
Ours: svc.Peer.PublicKey,
Theirs: pk,
}
ch, ok := svc.Peer.Backend.Offers[kp]
if ok {
ch <- o
}
}
return nil
}
func (svc *CandidateService) Remove(pk crypto.Key, result *bool) error {
return nil
}
type PeerService struct {
Peer *Peer
}
func NewPeerService(p *Peer) *PeerService {
return &PeerService{
Peer: p,
}
}
func (svc *PeerService) Add(p backend.Peer, result *bool) error {
return nil
}
func (svc *PeerService) Remove(p backend.Peer, result *bool) error {
return nil
}

203
pkg/backend/types.go Normal file
View File

@@ -0,0 +1,203 @@
package backend
import (
"encoding/json"
"fmt"
"net"
"net/url"
"time"
"riasc.eu/wice/pkg/crypto"
"github.com/pion/ice/v2"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
// Backend
type BackendType string // URL schemes
type BackendFactory func(*url.URL, map[string]string) (Backend, error)
type BackendPlugin struct {
New BackendFactory
Description string
}
// Candidates
type jsonCandidate struct {
Type string `json:"type"`
Foundation string `json:"foundation"`
Component int `json:"component"`
NetworkType string `json:"network"`
Priority int `json:"priority"`
Address string `json:"address"`
Port int `json:"port"`
TCPType *string `json:"tcp_type,omitempty"`
RelAddr *string `json:"related_address,omitempty"`
RelPort *int `json:"related_port,omitempty"`
}
type Candidate struct {
ice.Candidate
}
func (c *Candidate) MarshalJSON() ([]byte, error) {
jc := &jsonCandidate{
Type: c.Type().String(),
Foundation: c.Foundation(),
Component: int(c.Component()),
NetworkType: c.NetworkType().String(),
Priority: int(c.Priority()),
Address: c.Address(),
Port: c.Port(),
}
if c.TCPType() != ice.TCPTypeUnspecified {
t := c.TCPType().String()
jc.TCPType = &t
}
if r := c.RelatedAddress(); r != nil && r.Address != "" && r.Port != 0 {
jc.RelAddr = &r.Address
jc.RelPort = &r.Port
}
return json.Marshal(jc)
}
func (c *Candidate) UnmarshalJSON(data []byte) error {
var jc jsonCandidate
err := json.Unmarshal(data, &jc)
if err != nil {
return err
}
relAddr := ""
relPort := 0
if jc.RelAddr != nil && jc.RelPort != nil {
relAddr = *jc.RelAddr
relPort = int(*jc.RelPort)
}
tcpType := ice.TCPTypeUnspecified
if jc.TCPType != nil {
tcpType = ice.NewTCPType(*jc.TCPType)
}
var ic ice.Candidate
switch jc.Type {
case "host":
ic, err = ice.NewCandidateHost(&ice.CandidateHostConfig{
CandidateID: "",
Network: jc.NetworkType,
Address: jc.Address,
Port: int(jc.Port),
Component: uint16(jc.Component),
Priority: uint32(jc.Priority),
Foundation: jc.Foundation,
TCPType: tcpType})
case "srflx":
ic, err = ice.NewCandidateServerReflexive(&ice.CandidateServerReflexiveConfig{
CandidateID: "",
Network: jc.NetworkType,
Address: jc.Address,
Port: int(jc.Port),
Component: uint16(jc.Component),
Priority: uint32(jc.Priority),
Foundation: jc.Foundation,
RelAddr: relAddr,
RelPort: relPort,
})
case "prflx":
ic, err = ice.NewCandidatePeerReflexive(&ice.CandidatePeerReflexiveConfig{
CandidateID: "",
Network: jc.NetworkType,
Address: jc.Address,
Port: int(jc.Port),
Component: uint16(jc.Component),
Priority: uint32(jc.Priority),
Foundation: jc.Foundation,
RelAddr: relAddr,
RelPort: relPort,
})
case "relay":
ic, err = ice.NewCandidateRelay(&ice.CandidateRelayConfig{
CandidateID: "",
Network: jc.NetworkType,
Address: jc.Address,
Port: int(jc.Port),
Component: uint16(jc.Component),
Priority: uint32(jc.Priority),
Foundation: jc.Foundation,
RelAddr: relAddr,
RelPort: relPort,
OnClose: nil,
})
default:
err = fmt.Errorf("unknown candidate type: %s", jc.Type)
}
if err != nil {
return nil
}
c.Candidate = ic
return nil
}
// Peers
type jsonPeer struct {
PublicKey crypto.Key `json:"public_key"`
AllowedIPs []net.IPNet `json:"allowed_ips,omitempty"`
}
type Peer struct {
wgtypes.Peer
}
func (p *Peer) PublicKey() crypto.Key {
return crypto.Key(p.Peer.PublicKey)
}
func (p *Peer) MarshalJSON() ([]byte, error) {
jp := jsonPeer{
PublicKey: p.PublicKey(),
AllowedIPs: p.AllowedIPs,
}
return json.Marshal(jp)
}
func (p *Peer) UnmarshalJSON(data []byte) error {
var jp jsonPeer
return json.Unmarshal(data, &jp)
}
// Offers
type OfferMap map[crypto.Key]Offer
// SDP-like session description
// See: https://www.rfc-editor.org/rfc/rfc8866.html
type Offer struct {
ID int64 `json:"id"`
Version int64 `json:"version"`
Candidates []Candidate `json:"cands,omitempty"`
EndOfCandidates bool `json:"eoc,omitempty"`
CleartextSignature string `json:"signature"`
// Ufrag string `json:"ufrag,omitempty"`
// Pwd string `json:"pwd,omitempty"`
}
func NewOffer() Offer {
return Offer{
ID: time.Now().UnixNano(),
Version: 0,
Candidates: []Candidate{},
EndOfCandidates: false,
}
}

24
pkg/crypto/crypto.go Normal file
View File

@@ -0,0 +1,24 @@
package crypto
import (
"math/big"
"golang.org/x/crypto/curve25519"
)
// endecrypt encrypts a 32-byte slice given the their public & our private private curve25519 keys via simple XOR with the shared secret
func Curve25519Crypt(privKey, pubKey Key, payloadBuf []byte) ([]byte, error) {
// Perform static-static ECDH
keyBuf, err := curve25519.X25519(privKey[:], pubKey[:])
if err != nil {
return nil, err
}
var key, payload, enc big.Int
key.SetBytes(keyBuf)
payload.SetBytes(payloadBuf)
enc.Xor(&key, &payload)
return enc.Bytes(), nil
}

44
pkg/crypto/crypto_test.go Normal file
View File

@@ -0,0 +1,44 @@
package crypto_test
import (
"bytes"
"testing"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"riasc.eu/wice/internal/util"
"riasc.eu/wice/pkg/crypto"
)
func TestCurve25519Crypt(t *testing.T) {
keyA, err := wgtypes.GeneratePrivateKey()
if err != nil {
t.Fail()
}
keyB, err := wgtypes.GeneratePrivateKey()
if err != nil {
t.Fail()
}
pubA := keyA.PublicKey()
pubB := keyB.PublicKey()
payload, err := util.GenerateRandomBytes(32)
if err != nil {
t.Fail()
}
encPayload, err := crypto.Curve25519Crypt(keyA[:], pubB[:], payload)
if err != nil {
t.Fail()
}
decPayload, err := crypto.Curve25519Crypt(keyB[:], pubA[:], encPayload)
if err != nil {
t.Fail()
}
if !bytes.Equal(decPayload, payload) {
t.Fail()
}
}

131
pkg/crypto/jws.go Normal file
View File

@@ -0,0 +1,131 @@
package crypto
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/ucarion/jcs"
)
type JWK struct {
KeyType string `json:"kty"`
Curve string `json:"crv"`
X string `json:"x"`
}
type JWS struct {
Algorithm string `json:"alg"`
Key JWK `json:"jwk"`
}
func jsonCanonicalize(obj interface{}) (string, error) {
var objIntf interface{}
objJson, err := json.Marshal(obj)
if err != nil {
return "", err
}
err = json.Unmarshal(objJson, &objIntf)
if err != nil {
return "", err
}
msg, err := jcs.Format(objIntf)
if err != nil {
return "", err
}
return msg, nil
}
func JWSCTSign(obj interface{}, sk Key) (string, error) {
var pk Key
hdr := JWS{
Algorithm: "XEdDSA-25519",
Key: JWK{
KeyType: "OKP",
Curve: "X25519",
X: pk.String(),
},
}
msg, err := jsonCanonicalize(obj)
if err != nil {
return "", err
}
rnd := make([]byte, 32)
_, err = rand.Reader.Read(rnd[:])
if err != nil {
panic(err)
}
sig := sk.Sign([]byte(msg), rnd)
hdrBytes, err := json.Marshal(&hdr)
if err != nil {
return "", err
}
hdrBase64 := base64.URLEncoding.EncodeToString(hdrBytes)
sigBase64 := base64.URLEncoding.EncodeToString(sig[:])
plBase64 := "" // payload is always empty for JWS-CT
return fmt.Sprintf("%s.%s.%s", hdrBase64, plBase64, sigBase64), nil
}
func JWSCTVerify(obj interface{}, jwsStr string, pk Key) (bool, error) {
jwsStrParts := strings.Split(jwsStr, ".")
if len(jwsStrParts) != 3 {
return false, errors.New("invalid JWS format")
}
hdrBase64 := jwsStrParts[0]
plBase64 := jwsStrParts[1]
sigBase64 := jwsStrParts[2]
if plBase64 != "" {
return false, errors.New("payload field in JWS is not empty")
}
hdrBytes, err := base64.URLEncoding.DecodeString(hdrBase64)
if err != nil {
return false, err
}
sig, err := base64.URLEncoding.DecodeString(sigBase64)
if err != nil {
return false, err
}
var hdr JWS
err = json.Unmarshal(hdrBytes, &hdr)
if err != nil {
return false, err
}
if hdr.Key.KeyType != "OKP" {
return false, fmt.Errorf("Unsupported key type: %s", hdr.Key.KeyType)
}
if hdr.Key.Curve != "X25519" {
return false, fmt.Errorf("Unsupported curve type: %s", hdr.Key.Curve)
}
var ssig Signature
copy(ssig[:], sig)
msg, err := jsonCanonicalize(obj)
if err != nil {
return false, err
}
return pk.Verify([]byte(msg), ssig), nil
}

68
pkg/crypto/jws_test.go Normal file
View File

@@ -0,0 +1,68 @@
package crypto_test
import (
"fmt"
"testing"
"riasc.eu/wice/pkg/crypto"
)
type Person struct {
Name string
Age int
Children []Person
Signature string
}
func TestJWSCT(t *testing.T) {
einstein := Person{
Name: "Albert Einstein",
Age: 66,
Children: []Person{
{
Name: "Yoda",
Age: 9999,
},
},
}
sk, err := crypto.ParseKey("GMHOtIxfUrGmncORjYK/slCSK/8V2TF9MjzzoPDTkEc=")
if err != nil {
panic(err)
}
pk, err := crypto.ParseKey("Hxm0/KTFRGFirpOoTWO2iMde/gJX+oVswUXEzVN5En8=")
if err != nil {
panic(err)
}
einstein.Signature, err = crypto.JWSCTSign(&einstein, sk)
if err != nil {
t.Errorf("Failed to sign: %w", err)
}
sig := einstein.Signature
einstein.Signature = ""
fmt.Printf("Signature: %s\n", sig)
match, err := crypto.JWSCTVerify(&einstein, sig, pk)
if err != nil {
t.Errorf("Failed to verify: %w", err)
}
if !match {
t.Errorf("Signature mismatch")
}
einstein.Age = 67
match, err = crypto.JWSCTVerify(&einstein, sig, pk)
if err != nil {
t.Errorf("Failed to verify: %w", err)
}
if match {
t.Errorf("Signature false positive")
}
}

85
pkg/crypto/types.go Normal file
View File

@@ -0,0 +1,85 @@
package crypto
import (
"crypto/hmac"
"encoding/base64"
"github.com/pion/dtls/v2/pkg/crypto/hash"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
// Keys
const (
KeyLength = 32
SignatureLength = 64
)
type Key [KeyLength]byte
type Signature [SignatureLength]byte
type KeyPair struct {
Private Key
Public Key
}
type PublicKeyPair struct {
Ours Key
Theirs Key
}
func GeneratePrivateKey() (Key, error) {
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
return Key{}, err
}
return Key(key), nil
}
func ParseKey(str string) (Key, error) {
k, err := base64.StdEncoding.DecodeString(str)
if err != nil {
return Key{}, err
}
var key Key
copy(key[:], k[:KeyLength])
return key, nil
}
func (k Key) String() string {
return base64.StdEncoding.EncodeToString(k[:])
}
func (k Key) MarshalText() ([]byte, error) {
return []byte(k.String()), nil
}
func (k *Key) UnmarshalText(text []byte) error {
var err error
*k, err = ParseKey(string(text))
return err
}
func (k Key) PublicKey() Key {
key := wgtypes.Key(k)
return Key(key.PublicKey())
}
// Checks if the key is not zero
func (k Key) IsSet() bool {
return k != Key{}
}
func (kp PublicKeyPair) ID(key []byte) string {
ctx := hmac.New(hash.SHA512.CryptoHash().HashFunc().New, key)
ctx.Write(kp.Ours[:])
ctx.Write(kp.Theirs[:])
mac := ctx.Sum(nil)
return base64.URLEncoding.EncodeToString(mac)
}

111
pkg/crypto/types_test.go Normal file
View File

@@ -0,0 +1,111 @@
package crypto_test
import (
"encoding/json"
"testing"
"riasc.eu/wice/pkg/crypto"
)
func TestKeyString(t *testing.T) {
key1, err := crypto.GeneratePrivateKey()
if err != nil {
t.Fail()
}
keyString := key1.String()
key2, err := crypto.ParseKey(keyString)
if err != nil {
t.Fail()
}
if key1 != key2 {
t.Fail()
}
}
func TestGeneratePrivateKey(t *testing.T) {
key1, err := crypto.GeneratePrivateKey()
if err != nil {
t.Fail()
}
var zeroKey crypto.Key
if key1 == zeroKey {
t.Fail()
}
key2, err := crypto.GeneratePrivateKey()
if err != nil {
t.Fail()
}
if key1 == key2 {
t.Fail()
}
}
type testObj struct {
Key crypto.Key
}
func TestMarshal(t *testing.T) {
key, err := crypto.GeneratePrivateKey()
if err != nil {
t.Fail()
}
var obj1, obj2 testObj
obj1 = testObj{
Key: key,
}
objJson, err := json.Marshal(&obj1)
if err != nil {
t.Fail()
}
err = json.Unmarshal(objJson, &obj2)
if err != nil {
t.Fail()
}
if obj1 != obj2 {
t.Fail()
}
}
func TestPublicKey(t *testing.T) {
sk, err := crypto.ParseKey("GMHOtIxfUrGmncORjYK/slCSK/8V2TF9MjzzoPDTkEc=")
if err != nil {
t.Fail()
}
pk, err := crypto.ParseKey("Hxm0/KTFRGFirpOoTWO2iMde/gJX+oVswUXEzVN5En8=")
if err != nil {
t.Fail()
}
if sk.PublicKey() != pk {
t.Fail()
}
}
func TestIsSet(t *testing.T) {
key, err := crypto.GeneratePrivateKey()
if err != nil {
t.Fail()
}
if !key.IsSet() {
t.Fail()
}
key = crypto.Key{}
if key.IsSet() {
t.Fail()
}
}

92
pkg/crypto/xeddsa.go Normal file
View File

@@ -0,0 +1,92 @@
package crypto
import (
"crypto/sha512"
"github.com/Scratch-net/vxeddsa/edwards25519"
"golang.org/x/crypto/ed25519"
)
// sign signs the message with privateKey and returns a signature as a byte slice.
func (sk Key) Sign(msg []byte, rnd []byte) Signature {
// Calculate Ed25519 public key from Curve25519 private key
var A edwards25519.ExtendedGroupElement
var pk [32]byte
edwards25519.GeScalarMultBase(&A, (*[32]byte)(&sk))
A.ToBytes(&pk)
// Calculate r
diversifier := []byte{
0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}
var r [64]byte
hash := sha512.New()
hash.Write(diversifier)
hash.Write(sk[:])
hash.Write(msg)
hash.Write(rnd)
hash.Sum(r[:0])
// Calculate R
var rReduced [32]byte
edwards25519.ScReduce(&rReduced, &r)
var R edwards25519.ExtendedGroupElement
edwards25519.GeScalarMultBase(&R, &rReduced)
var encr [32]byte
R.ToBytes(&encr)
// Calculate S = r + SHA2-512(R || A_ed || msg) * a (mod L)
var hramDigest [64]byte
hash.Reset()
hash.Write(encr[:])
hash.Write(pk[:])
hash.Write(msg)
hash.Sum(hramDigest[:0])
var hramDigestReduced [32]byte
edwards25519.ScReduce(&hramDigestReduced, &hramDigest)
var s [32]byte
edwards25519.ScMulAdd(&s, &hramDigestReduced, (*[32]byte)(&sk), &rReduced)
var sig Signature
copy(sig[:32], encr[:])
copy(sig[32:], s[:])
sig[63] |= pk[31] & 0x80
return sig
}
// verify checks whether the message has a valid signature.
func (pk Key) Verify(msg []byte, sig Signature) bool {
pk[31] &= 0x7F
/* Convert the Curve25519 public key into an Ed25519 public key. In
particular, convert Curve25519's "montgomery" x-coordinate into an
Ed25519 "edwards" y-coordinate:
ed_y = (mont_x - 1) / (mont_x + 1)
NOTE: mont_x=-1 is converted to ed_y=0 since fe_invert is mod-exp
Then move the sign bit into the pubkey from the signature.
*/
var edY, one, montX, montXMinusOne, montXPlusOne edwards25519.FieldElement
edwards25519.FeFromBytes(&montX, (*[32]byte)(&pk))
edwards25519.FeOne(&one)
edwards25519.FeSub(&montXMinusOne, &montX, &one)
edwards25519.FeAdd(&montXPlusOne, &montX, &one)
edwards25519.FeInvert(&montXPlusOne, &montXPlusOne)
edwards25519.FeMul(&edY, &montXMinusOne, &montXPlusOne)
var A_ed [32]byte
edwards25519.FeToBytes(&A_ed, &edY)
A_ed[31] |= sig[63] & 0x80
sig[63] &= 0x7F
return ed25519.Verify(A_ed[:], msg, sig[:])
}

49
pkg/crypto/xeddsa_test.go Normal file
View File

@@ -0,0 +1,49 @@
package crypto_test
import (
"crypto/rand"
"fmt"
"testing"
"riasc.eu/wice/pkg/crypto"
)
func TestXEdDSA(t *testing.T) {
sk, err := crypto.GeneratePrivateKey()
if err != nil {
t.Fail()
}
pk := sk.PublicKey()
msg := make([]byte, 200)
rnd := make([]byte, 32)
_, err = rand.Reader.Read(rnd[:])
if err != nil {
t.Fail()
}
_, err = rand.Reader.Read(msg[:])
if err != nil {
t.Fail()
}
signature := sk.Sign(msg, rnd)
fmt.Printf("PrivateKey = %s\n", sk)
fmt.Printf("PublicKey = %s\n", pk)
fmt.Printf("Signature = %s\n", signature)
res := pk.Verify(msg, signature)
if !res {
t.Error("Signature mismatch")
}
msg[0] ^= 0xff
res = pk.Verify(msg, signature)
if res {
t.Error("Signature false positive")
}
}

379
pkg/intf/base.go Normal file
View File

@@ -0,0 +1,379 @@
package intf
import (
"fmt"
"io"
"math/rand"
"os"
"os/exec"
"sort"
"strings"
"time"
"github.com/pion/ice/v2"
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"riasc.eu/wice/internal/util"
"riasc.eu/wice/internal/wg"
"riasc.eu/wice/pkg/args"
"riasc.eu/wice/pkg/backend"
"riasc.eu/wice/pkg/crypto"
)
const (
PeerModifiedEndpoint = (1 << 0)
PeerModifiedKeepaliveInterval = (1 << 1)
PeerModifiedProtocolVersion = (1 << 2)
PeerModifiedAllowedIPs = (1 << 3)
PeerModifiedHandshakeTime = (1 << 4)
)
type BaseInterface struct {
wgtypes.Device
peers map[crypto.Key]Peer
lastSync time.Time
logger *log.Entry
client *wgctrl.Client
mux ice.UDPMux
backend backend.Backend
args *args.Args
}
func (i *BaseInterface) Name() string {
return i.Device.Name
}
func (i *BaseInterface) PublicKey() crypto.Key {
return crypto.Key(i.Device.PublicKey)
}
func (i *BaseInterface) PrivateKey() crypto.Key {
return crypto.Key(i.Device.PrivateKey)
}
func (i *BaseInterface) Close() error {
i.logger.Info("Closing interface")
for _, p := range i.peers {
err := p.Close()
if err != nil {
return err
}
}
return nil
}
func (i *BaseInterface) DumpConfig(wr io.Writer) {
fmt.Fprintf(wr, "[Interface] # %s\n", i.Name())
if i.PublicKey().IsSet() {
fmt.Fprintf(wr, "PublicKey = %s\n", i.PublicKey)
}
if i.PrivateKey().IsSet() {
fmt.Fprintf(wr, "PrivateKey = %s\n", i.PrivateKey)
}
if i.ListenPort != 0 {
fmt.Fprintf(wr, "ListenPort = %d\n", i.ListenPort)
}
if i.FirewallMark != 0 {
fmt.Fprintf(wr, "FwMark = %#x\n", i.FirewallMark)
}
for _, p := range i.Peers {
fmt.Fprintf(wr, "[Peer]\n")
if crypto.Key(p.PublicKey).IsSet() {
fmt.Fprintf(wr, "PublicKey = %s\n", p.PublicKey)
}
if crypto.Key(p.PresharedKey).IsSet() {
fmt.Fprintf(wr, "PresharedKey = %s\n", p.PresharedKey)
}
if !p.LastHandshakeTime.Equal(time.Time{}) {
fmt.Fprintf(wr, "LastHandshakeTime = %v\n", p.LastHandshakeTime)
}
if p.PersistentKeepaliveInterval.Seconds() != 0 {
fmt.Fprintf(wr, "PersistentKeepalive = %d # seconds\n", int(p.PersistentKeepaliveInterval.Seconds()))
}
if len(p.AllowedIPs) > 0 {
aIPs := []string{}
for _, aIP := range p.AllowedIPs {
aIPs = append(aIPs, aIP.String())
}
fmt.Fprintf(wr, "AllowedIPs = %s\n", strings.Join(aIPs, ", "))
}
if p.Endpoint != nil {
fmt.Fprintf(wr, "Endpoint = %s\n", p.Endpoint.String())
}
}
}
func (i *BaseInterface) syncPeer(oldPeer, newPeer *wgtypes.Peer) error {
modified := 0
// Compare peer properties
if util.CmpEndpoint(newPeer.Endpoint, oldPeer.Endpoint) != 0 {
modified |= PeerModifiedEndpoint
}
if newPeer.ProtocolVersion != oldPeer.ProtocolVersion {
modified |= PeerModifiedProtocolVersion
}
if newPeer.PersistentKeepaliveInterval != oldPeer.PersistentKeepaliveInterval {
modified |= PeerModifiedKeepaliveInterval
}
if newPeer.LastHandshakeTime != oldPeer.LastHandshakeTime {
modified |= PeerModifiedHandshakeTime
}
if len(newPeer.AllowedIPs) != len(oldPeer.AllowedIPs) {
modified |= PeerModifiedAllowedIPs
} else {
for i := 0; i < len(oldPeer.AllowedIPs); i++ {
if util.CmpNet(&oldPeer.AllowedIPs[i], &newPeer.AllowedIPs[i]) != 0 {
modified |= PeerModifiedAllowedIPs
break
}
}
}
// Find changes in AllowedIP list
// sort.Slice(newPeer.AllowedIPs, lessNets(newPeer.AllowedIPs))
// sort.Slice(oldPeer.AllowedIPs, lessNets(oldPeer.AllowedIPs))
// for i, j := 0, 0; i < len(oldPeer.AllowedIPs) && j < len(newPeer.AllowedIPs); {
// oldAllowedIP := &oldPeer.AllowedIPs[i]
// newAllowedIP := &newPeer.AllowedIPs[j]
// cmp := cmpNet(oldAllowedIP, newAllowedIP)
// switch {
// case cmp < 0: // deleted
// d.onPeerAllowedIPDeleted(oldPeer)
// case cmp > 0: // added
// d.onPeerAllowedIPAdded(newPeer)
// default: //
// i++
// j++
// }
// }
if modified != 0 {
i.logger.WithField("peer", oldPeer.PublicKey).WithField("modified", modified).Info("Peer modified")
i.onPeerModified(oldPeer, newPeer, modified)
}
return nil
}
func (i *BaseInterface) Sync(newDev *wgtypes.Device) error {
// Compare device properties
if newDev.Type != i.Type {
i.logger.WithField("old", i.Type).WithField("new", newDev.Type).Info("Type changed")
i.Device.Type = newDev.Type
}
if newDev.FirewallMark != i.FirewallMark {
i.logger.WithField("old", i.FirewallMark).WithField("new", newDev.FirewallMark).Info("FirewallMark changed")
i.Device.FirewallMark = newDev.FirewallMark
}
if newDev.PrivateKey != i.Device.PrivateKey {
i.logger.WithField("old", i.PrivateKey).WithField("new", newDev.PrivateKey).Info("PrivateKey changed")
i.Device.PrivateKey = newDev.PrivateKey
i.Device.PublicKey = newDev.PublicKey
}
if newDev.ListenPort != i.ListenPort {
i.logger.WithField("old", i.ListenPort).WithField("new", newDev.ListenPort).Info("ListenPort changed")
i.Device.ListenPort = newDev.ListenPort
}
sort.Slice(newDev.Peers, wg.LessPeers(newDev.Peers))
sort.Slice(i.Device.Peers, wg.LessPeers(i.Peers))
k, j := 0, 0
for k < len(i.Peers) && j < len(newDev.Peers) {
oldPeer := &i.Peers[k]
newPeer := &newDev.Peers[j]
cmp := wg.CmpPeers(oldPeer, newPeer)
switch {
case cmp < 0: // removed
i.logger.WithField("peer", oldPeer.PublicKey).Info("Peer removed")
i.onPeerRemoved(oldPeer)
k++
case cmp > 0: // added
i.logger.WithField("peer", oldPeer.PublicKey).Info("Peer added")
i.onPeerAdded(newPeer)
j++
default: //
i.syncPeer(oldPeer, newPeer)
k++
j++
}
}
for k < len(i.Peers) {
oldPeer := &i.Peers[k]
i.logger.WithField("peer", oldPeer.PublicKey).Info("Peer removed")
i.onPeerRemoved(oldPeer)
k++
}
for j < len(newDev.Peers) {
newPeer := &newDev.Peers[j]
i.logger.WithField("peer", newPeer.PublicKey).Info("Peer added")
i.onPeerAdded(newPeer)
j++
}
i.Peers = newDev.Peers
i.lastSync = time.Now()
return nil
}
func (i *BaseInterface) SyncConfig(cfg string) error {
_, err := os.Stat(cfg)
if err != nil {
return err
}
cmd := exec.Command("wg", "syncconf", i.Name(), cfg)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to sync configuration: %w\n%s", err, output)
}
i.logger.WithField("config", cfg).Debug("Synced configuration")
return nil
}
func (i *BaseInterface) onPeerAdded(p *wgtypes.Peer) {
peer, err := NewPeer(p, i)
if err != nil {
i.logger.WithError(err).WithField("peer", peer.PublicKey).Fatal("Failed to setup peer")
}
i.peers[peer.PublicKey()] = peer
}
func (i *BaseInterface) onPeerRemoved(peer *wgtypes.Peer) {
p, ok := i.peers[crypto.Key(peer.PublicKey)]
if !ok {
i.logger.WithField("peer", peer.PublicKey).Warn("Failed to find matching peer")
}
err := p.Close()
if err != nil {
i.logger.WithField("peer", peer.PublicKey).Warn("Failed to close peer")
}
delete(i.peers, crypto.Key(peer.PublicKey))
}
func (i *BaseInterface) onPeerModified(old, new *wgtypes.Peer, modified int) {
p, ok := i.peers[crypto.Key(new.PublicKey)]
if ok {
p.OnModified(new, modified)
} else {
i.logger.Error("Failed to find modified peer")
}
}
func (i *BaseInterface) AddPeer(pk wgtypes.Key) error {
cfg := wgtypes.Config{
Peers: []wgtypes.PeerConfig{
{
PublicKey: pk,
},
},
}
return i.client.ConfigureDevice(i.Name(), cfg)
}
func (i *BaseInterface) RemovePeer(pk wgtypes.Key) error {
cfg := wgtypes.Config{
Peers: []wgtypes.PeerConfig{
{
PublicKey: pk,
Remove: true,
},
},
}
return i.client.ConfigureDevice(i.Name(), cfg)
}
func NewInterface(dev *wgtypes.Device, client *wgctrl.Client, backend backend.Backend, args *args.Args) (BaseInterface, error) {
i := BaseInterface{
Device: *dev,
client: client,
backend: backend,
args: args,
logger: log.WithFields(log.Fields{
"intf": dev.Name,
"type": "kernel",
}),
peers: make(map[crypto.Key]Peer),
}
i.logger.Info("Creating new interface")
// Sync config
if i.args.ConfigSync {
cfg := fmt.Sprintf("%s/%s.conf", i.args.ConfigPath, i.Name())
err := i.SyncConfig(cfg)
if err != nil {
return BaseInterface{}, fmt.Errorf("failed to sync interface configuration: %w", err)
}
}
// Fixup device config
err := i.Fixup()
if err != nil {
return BaseInterface{}, fmt.Errorf("failed to fix interface configuration: %w", err)
}
// We remove all peers here so that they get added by the following sync
i.Peers = nil
i.Sync(dev)
return i, nil
}
func (i *BaseInterface) Fixup() error {
var cfg wgtypes.Config
if !i.PrivateKey().IsSet() {
if i.Type != wgtypes.Userspace {
i.logger.Warn("Device has no private key. Generating one..")
}
key, _ := wgtypes.GeneratePrivateKey()
cfg.PrivateKey = &key
}
if i.ListenPort == 0 {
i.logger.Warn("Device has no listen port. Setting a random one..")
// Ephemeral Port Range (RFC6056 Sect. 2.1)
portMin := (1 << 15) + (1 << 14)
portMax := (1 << 16)
port := portMin + rand.Intn(portMax-portMin)
cfg.ListenPort = &port
}
err := i.client.ConfigureDevice(i.Name(), cfg)
if err != nil {
return fmt.Errorf("failed to configure device: %w", err)
}
return nil
}

34
pkg/intf/credentials.go Normal file
View File

@@ -0,0 +1,34 @@
package intf
import (
"encoding/base64"
"riasc.eu/wice/pkg/crypto"
)
// LocalCredentials returns local ufrag, pwd of a peer
// ufrag is base64 encoded public key of the peer which wants to connect
// pwd is the base64 encoded and encrypted public key of our interface
// for encryption the public key of the peer is used
func (p *Peer) LocalCreds() (string, string, error) {
pl := p.Interface.PublicKey()
enc, err := crypto.Curve25519Crypt(p.Interface.PrivateKey(), p.PublicKey(), pl[:])
if err != nil {
return "", "", err
}
return base64.StdEncoding.EncodeToString(p.Peer.PublicKey[:]),
base64.StdEncoding.EncodeToString(enc), nil
}
// RemoteCredentials returns remote ufrag, pwd of a peer
func (p *Peer) RemoteCredentials() (string, string, error) {
pl := p.PublicKey()
enc, err := crypto.Curve25519Crypt(p.Interface.PrivateKey(), p.PublicKey(), pl[:])
if err != nil {
return "", "", err
}
return p.Interface.PublicKey().String(),
base64.StdEncoding.EncodeToString(enc), nil
}

15
pkg/intf/devices.go Normal file
View File

@@ -0,0 +1,15 @@
package intf
import "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
type Devices []*wgtypes.Device
func (devs *Devices) GetByName(name string) *wgtypes.Device {
for _, dev := range *devs {
if dev.Name == name {
return dev
}
}
return nil
}

20
pkg/intf/interface.go Normal file
View File

@@ -0,0 +1,20 @@
package intf
import (
"io"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
type Interface interface {
io.Closer
DumpConfig(io.Writer)
AddPeer(peer wgtypes.Key) error
RemovePeer(peer wgtypes.Key) error
Sync(*wgtypes.Device) error
Name() string
}

120
pkg/intf/interfaces.go Normal file
View File

@@ -0,0 +1,120 @@
package intf
import (
"fmt"
"os"
log "github.com/sirupsen/logrus"
"riasc.eu/wice/pkg/args"
be "riasc.eu/wice/pkg/backend"
"golang.zx2c4.com/wireguard/wgctrl"
)
type Interfaces []Interface
func (interfaces *Interfaces) GetByName(name string) Interface {
for _, intf := range *interfaces {
if intf.Name() == name {
return intf
}
}
return nil
}
func (interfaces *Interfaces) CloseAll() {
for _, intf := range *interfaces {
intf.Close()
}
}
func (interfaces *Interfaces) SyncAll(client *wgctrl.Client, backend be.Backend, args *args.Args) error {
devices, err := client.Devices()
if err != nil {
log.WithError(err).Fatal("Failed to list Wireguard interfaces")
}
syncedInterfaces := Interfaces{}
keepInterfaces := Interfaces{}
for _, device := range devices {
if !args.InterfaceRegex.Match([]byte(device.Name)) {
continue // Skip interfaces which dont match the filter
}
// Find matching interface
intf := interfaces.GetByName(device.Name)
if intf == nil { // new interface
log.WithField("intf", device.Name).Info("Adding new interface")
i, err := NewInterface(device, client, backend, args)
if err != nil {
log.WithField("intf", device.Name).WithError(err).Fatalf("Failed to create new interface")
}
intf = &i
*interfaces = append(*interfaces, &i)
} else { // existing interface
log.WithField("intf", intf.Name()).Trace("Sync existing interface")
err := intf.Sync(device)
if err != nil {
log.WithError(err).Fatal("Failed to sync interface %s", intf.Name())
}
}
syncedInterfaces = append(syncedInterfaces, intf)
}
for _, intf := range *interfaces {
i := syncedInterfaces.GetByName(intf.Name())
if i == nil {
log.WithField("intf", intf.Name()).Info("Removing vanished interface")
err := intf.Close()
if err != nil {
log.WithError(err).Fatal("Failed to close interface")
}
} else {
keepInterfaces = append(keepInterfaces, intf)
}
}
*interfaces = keepInterfaces
return nil
}
func (interfaces *Interfaces) CreateFromArgs(client *wgctrl.Client, backend be.Backend, args *args.Args) error {
var devs Devices
devs, err := client.Devices()
if err != nil {
return err
}
for _, i := range args.Interfaces {
dev := devs.GetByName(i)
if dev != nil {
log.WithField("intf", i).Warn("Interface already exists. Skipping..")
continue
}
var intf Interface
if args.User {
intf, err = CreateUserInterface(i, client, backend, args)
} else {
intf, err = CreateKernelInterface(i, client, backend, args)
}
if err != nil {
return fmt.Errorf("failed to create Wireguard device: %w", err)
}
if log.GetLevel() >= log.DebugLevel {
log.Debug("Intialized interface:")
intf.DumpConfig(os.Stdout)
}
*interfaces = append(*interfaces, intf)
}
return nil
}

109
pkg/intf/kernel.go Normal file
View File

@@ -0,0 +1,109 @@
package intf
import (
"fmt"
log "github.com/sirupsen/logrus"
"github.com/vishvananda/netlink"
"golang.zx2c4.com/wireguard/wgctrl"
"riasc.eu/wice/pkg/args"
"riasc.eu/wice/pkg/backend"
nl "riasc.eu/wice/pkg/netlink"
)
type KernelInterface struct {
BaseInterface
created bool
link netlink.Link
}
func (i *KernelInterface) Close() error {
err := i.BaseInterface.Close()
if err != nil {
return err
}
if i.created {
err := i.Delete()
if err != nil {
return err
}
}
return nil
}
func (i *KernelInterface) Delete() error {
i.logger.Debug("Deleting kernel device")
l := &nl.Wireguard{
LinkAttrs: netlink.NewLinkAttrs(),
}
l.LinkAttrs.Name = i.Name()
err := netlink.LinkDel(l)
if err != nil {
return fmt.Errorf("failed to delete Wireguard device: %w", err)
}
return nil
}
func (i *KernelInterface) SetMTU(mtu int) error {
i.logger.Debug("Set link MTU")
return netlink.LinkSetMTU(i.link, mtu)
}
func (i *KernelInterface) SetUp() error {
i.logger.Debug("Set link up")
return netlink.LinkSetUp(i.link)
}
func (i *KernelInterface) SetDown(mtu int) error {
i.logger.Debug("Set link down")
return netlink.LinkSetDown(i.link)
}
func CreateKernelInterface(name string, client *wgctrl.Client, backend backend.Backend, args *args.Args) (Interface, error) {
log.WithField("intf", name).Debug("Creating new kernel interface")
l := &nl.Wireguard{
LinkAttrs: netlink.NewLinkAttrs(),
}
l.LinkAttrs.Name = name
err := netlink.LinkAdd(l)
if err != nil {
return nil, fmt.Errorf("failed to create Wireguard interface: %w", err)
}
link, err := netlink.LinkByName(name)
if err != nil {
return nil, fmt.Errorf("failed to get link details: %w", err)
}
// Connect to UAPI
wgDev, err := client.Device(name)
if err != nil {
return nil, err
}
baseDev, err := NewInterface(wgDev, client, backend, args)
if err != nil {
return nil, err
}
i := &KernelInterface{
BaseInterface: baseDev,
created: true,
link: link,
}
err = i.SetUp()
if err != nil {
return nil, fmt.Errorf("failed to bring link %s up: %w", name, err)
}
return i, nil
}

395
pkg/intf/peer.go Normal file
View File

@@ -0,0 +1,395 @@
package intf
import (
"context"
"encoding/base64"
"fmt"
"math/big"
"net"
"os"
"runtime"
"strings"
"time"
log "github.com/sirupsen/logrus"
"github.com/pion/ice/v2"
"golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"riasc.eu/wice/pkg/args"
"riasc.eu/wice/pkg/backend"
"riasc.eu/wice/pkg/crypto"
"riasc.eu/wice/pkg/proxy"
)
type Peer struct {
wgtypes.Peer
Interface *BaseInterface
ICEAgent *ice.Agent
ICEConn *ice.Conn
localOffer backend.Offer
lastRemoteOfferID int64
remoteOffers chan backend.Offer
selectedCandidatePairs chan *ice.CandidatePair
LastHandshake time.Time
logger *log.Entry
client *wgctrl.Client
args *args.Args
backend backend.Backend
}
func (p *Peer) Close() error {
err := p.backend.WithdrawOffer(p.PublicKeyPair())
if err != nil {
return err
}
p.ICEAgent.Close()
p.logger.Info("Closing peer")
return nil
}
func (p *Peer) String() string {
return p.PublicKey().String()
}
func (p *Peer) UpdateEndpoint(addr *net.UDPAddr) error {
peerCfg := wgtypes.PeerConfig{
PublicKey: p.Peer.PublicKey,
UpdateOnly: true,
ReplaceAllowedIPs: false,
Endpoint: addr,
}
cfg := wgtypes.Config{
Peers: []wgtypes.PeerConfig{peerCfg},
}
return p.client.ConfigureDevice(p.Interface.Name(), cfg)
}
func (p *Peer) colorizeConnectionState(state ice.ConnectionState) string {
if runtime.GOOS != "windows" {
var color int
switch state {
case ice.ConnectionStateNew:
fallthrough
case ice.ConnectionStateCompleted:
fallthrough
case ice.ConnectionStateClosed:
color = 37
case ice.ConnectionStateDisconnected:
fallthrough
case ice.ConnectionStateChecking:
color = 33
case ice.ConnectionStateFailed:
color = 31
case ice.ConnectionStateConnected:
color = 32
}
return fmt.Sprintf("\x1b[%d;1m%s\x1b[0m", color, state)
} else {
return state.String()
}
}
func (p *Peer) isControlling() bool {
var pkOur, pkTheir big.Int
pkOur.SetBytes(p.Interface.Device.PublicKey[:])
pkTheir.SetBytes(p.Peer.PublicKey[:])
return pkOur.Cmp(&pkTheir) == -1 // the smaller PK is controlling
}
func (p *Peer) OnModified(new *wgtypes.Peer, modified int) {
if modified&PeerModifiedHandshakeTime > 0 {
p.LastHandshake = new.LastHandshakeTime
p.logger.WithField("time", new.LastHandshakeTime).Debug("New handshake")
}
}
func (p *Peer) onCandidate(c ice.Candidate) {
if c == nil {
p.logger.Info("Candidate gathering completed")
p.localOffer.EndOfCandidates = true
} else {
p.logger.WithField("candidate", c).Info("Found new local candidate")
p.localOffer.Candidates = append(p.localOffer.Candidates, backend.Candidate{
Candidate: c,
})
}
p.localOffer.Version++
if err := p.backend.PublishOffer(p.PublicKeyPair(), p.localOffer); err != nil {
p.logger.WithError(err).Warn("Failed to publish offer")
os.Exit(-1)
}
}
func (p *Peer) onConnectionStateChange(state ice.ConnectionState) {
p.logger.WithField("state", strings.ToLower(state.String())).Infof("Connection state changed: %s", p.colorizeConnectionState(state))
if state == ice.ConnectionStateFailed {
go p.restartLocal()
}
}
func (p *Peer) onSelectedCandidatePairChange(a, b ice.Candidate) {
cp, err := p.ICEAgent.GetSelectedCandidatePair()
if err != nil {
p.logger.Warn("Failed to get selected candidate pair")
}
p.logger.WithField("pair", cp).Info("Selected new candidate pair")
p.selectedCandidatePairs <- cp
}
func (p *Peer) onOffer(o backend.Offer) {
if p.lastRemoteOfferID > 0 && p.lastRemoteOfferID != o.ID { // && o.Version == 0 {
p.restartRemote(o)
}
for _, c := range o.Candidates {
err := p.ICEAgent.AddRemoteCandidate(c)
if err != nil {
p.logger.WithError(err).Fatal("Failed to add remote candidate")
}
p.logger.WithField("candidate", c).Debug("Add remote candidate")
}
p.lastRemoteOfferID = o.ID
}
func (p *Peer) restartLocal() {
p.logger.WithField("id", p.localOffer.ID).Infof("Restarting session triggered locally in %s", p.args.RestartInterval)
time.Sleep(p.args.RestartInterval)
p.localOffer = backend.NewOffer()
p.backend.PublishOffer(p.PublicKeyPair(), p.localOffer)
offer := <-p.remoteOffers // wait for remote answer
p.lastRemoteOfferID = offer.ID
p.restart(offer)
}
func (p *Peer) restartRemote(offer backend.Offer) {
p.logger.WithField("id", p.localOffer.ID).Info("Restarting session triggered locally")
id := p.localOffer.ID
p.localOffer = backend.NewOffer()
p.localOffer.ID = id // we keep our offer ID when restart is triggered by remote
p.restart(offer)
}
// Performs an ICE restart
//
// This restart can either be triggered by a failed
// ICE connection state (Peer.onConnectionState())
// or by a remote offer which indicates a restart (Peer.onOffer())
func (p *Peer) restart(offer backend.Offer) {
var err error
var localUfrag, localPwd, remoteUfrag, remotePwd string
if remoteUfrag, remotePwd, err = p.RemoteCredentials(); err != nil {
p.logger.WithError(err).Error("Failed to get remote credentials")
return
}
if localUfrag, localPwd, err = p.LocalCreds(); err != nil {
p.logger.WithError(err).Error("Failed to get remote credentials")
return
}
if err := p.ICEAgent.Restart(localUfrag, localPwd); err != nil {
p.logger.WithError(err).Error("Failed to restart ICE session")
return
}
for _, cand := range offer.Candidates {
err = p.ICEAgent.AddRemoteCandidate(cand.Candidate)
if err != nil {
p.logger.WithError(err).Error("Failed to add remote candidate")
}
}
if err := p.ICEAgent.GatherCandidates(); err != nil {
p.logger.WithError(err).Error("Failed to gather candidates")
return
}
if err := p.ICEAgent.SetRemoteCredentials(remoteUfrag, remotePwd); err != nil {
p.logger.WithError(err).Error("Failed to set remote creds")
return
}
}
func (p *Peer) start(remoteUfrag, remotePwd string) {
var err error
p.logger.WithField("id", p.localOffer.ID).Info("Starting new session")
p.remoteOffers, err = p.backend.SubscribeOffer(p.PublicKeyPair())
if err != nil {
p.logger.WithError(err).Fatal("Failed to subscribe to offers")
}
// Wait for first offer from remote agent before creating ICE connection
o := <-p.remoteOffers
p.onOffer(o)
// Start the ICE Agent. One side must be controlled, and the other must be controlling
if p.isControlling() {
p.ICEConn, err = p.ICEAgent.Dial(context.TODO(), remoteUfrag, remotePwd)
} else {
p.ICEConn, err = p.ICEAgent.Accept(context.TODO(), remoteUfrag, remotePwd)
}
if err != nil {
p.logger.WithError(err).Fatal("Failed to establish ICE connection")
}
// Wait until we are ready
var currentProxy proxy.Proxy = nil
for {
select {
// New remote candidate
case offer := <-p.remoteOffers:
p.onOffer(offer)
// New selected candidate pair
case cp := <-p.selectedCandidatePairs:
pt := p.args.ProxyType
// p.logger.Infof("Conntype: %+v", reflect.ValueOf(cp.Local).Elem().Type())
isTCPRelayCandidate := cp.Local.Type() == ice.CandidateTypeRelay
if isTCPRelayCandidate {
pt = proxy.ProxyTypeUser
}
if currentProxy != nil && proxy.Type(currentProxy) == pt {
// Update endpoint of existing proxy
addr := p.ICEConn.RemoteAddr().(*net.UDPAddr)
currentProxy.UpdateEndpoint(addr)
} else {
// Stop old proxy
if currentProxy != nil {
currentProxy.Close()
}
ident := base64.StdEncoding.EncodeToString(p.Peer.PublicKey[:16])
// Replace proxy
if currentProxy, err = proxy.NewProxy(pt, ident, p.Interface.ListenPort, p.UpdateEndpoint, p.ICEConn); err != nil {
p.logger.WithError(err).Fatal("Failed to setup proxy")
}
}
}
}
}
func (p *Peer) PublicKey() crypto.Key {
return crypto.Key(p.Peer.PublicKey)
}
func (p *Peer) PublicKeyPair() crypto.PublicKeyPair {
return crypto.PublicKeyPair{
Ours: p.Interface.PublicKey(),
Theirs: p.PublicKey(),
}
}
func NewPeer(wgp *wgtypes.Peer, i *BaseInterface) (Peer, error) {
var err error
p := Peer{
Interface: i,
Peer: *wgp,
client: i.client,
backend: i.backend,
lastRemoteOfferID: -1,
localOffer: backend.NewOffer(),
args: i.args,
selectedCandidatePairs: make(chan *ice.CandidatePair),
logger: log.WithFields(log.Fields{
"intf": i.Name,
"peer": wgp.PublicKey.String(),
}),
}
agentConfig := p.args.AgentConfig
agentConfig.InterfaceFilter = func(name string) bool {
_, err := p.client.Device(name)
return p.args.IceInterfaceRegex.Match([]byte(name)) && err != nil
}
var localUfrag, localPwd, remoteUfrag, remotePwd string
if localUfrag, localPwd, err = p.LocalCreds(); err != nil {
return Peer{}, fmt.Errorf("failed to get local credentials: %w", err)
}
if remoteUfrag, remotePwd, err = p.RemoteCredentials(); err != nil {
return Peer{}, fmt.Errorf("failed to get remote credentials: %w", err)
}
p.logger.WithFields(log.Fields{
"ufrag_local": localUfrag,
"pwd_local": localPwd,
"ufrag_remote": remoteUfrag,
"pwd_remote": remotePwd,
}).Debug("Peer credentials")
agentConfig.LocalUfrag = localUfrag
agentConfig.LocalPwd = localPwd
if p.args.ProxyType == proxy.ProxyTypeEBPF {
proxy.SetupEBPFMux(&agentConfig, p.Interface.ListenPort)
}
if p.ICEAgent, err = ice.NewAgent(&agentConfig); err != nil {
return Peer{}, fmt.Errorf("failed to create ICE agent: %w", err)
}
// When we have gathered a new ICE Candidate send it to the remote peer
if err := p.ICEAgent.OnCandidate(p.onCandidate); err != nil {
return Peer{}, fmt.Errorf("failed to setup on candidate handler: %w", err)
}
// When selected candidate pair changes
if err := p.ICEAgent.OnSelectedCandidatePairChange(p.onSelectedCandidatePairChange); err != nil {
return Peer{}, fmt.Errorf("failed to setup on selected candidate pair handler: %w", err)
}
// When ICE Connection state has change print to stdout
if err := p.ICEAgent.OnConnectionStateChange(p.onConnectionStateChange); err != nil {
return Peer{}, fmt.Errorf("failed to setup on connection state handler: %w", err)
}
p.logger.Info("Gathering local candidates")
if err := p.ICEAgent.GatherCandidates(); err != nil {
return Peer{}, fmt.Errorf("failed to gather candidates: %w", err)
}
go p.start(remoteUfrag, remotePwd)
return p, nil
}

134
pkg/intf/user.go Normal file
View File

@@ -0,0 +1,134 @@
// +build !windows
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2021 WireGuard LLC. All Rights Reserved.
*/
package intf
import (
"fmt"
"net"
log "github.com/sirupsen/logrus"
"riasc.eu/wice/pkg/args"
"riasc.eu/wice/pkg/backend"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/ipc"
"golang.zx2c4.com/wireguard/tun"
"golang.zx2c4.com/wireguard/wgctrl"
)
type UserDevice struct {
BaseInterface
tun tun.Device
log *device.Logger
userDevice *device.Device
userAPI net.Listener
}
func newLogger(log *log.Entry) *device.Logger {
logger := log.WithField("logger", "wireguard")
return &device.Logger{
Verbosef: logger.Debugf,
Errorf: logger.Errorf,
}
}
func (i *UserDevice) Close() error {
err := i.userAPI.Close()
if err != nil {
return err
}
i.userDevice.Close()
err = i.BaseInterface.Close()
if err != nil {
return err
}
return nil
}
func (i *UserDevice) handleUserApi() {
for {
conn, err := i.userAPI.Accept()
if err != nil {
i.logger.WithError(err).Warn("Failed to accept UAPI connection")
return
}
go i.userDevice.IpcHandle(conn)
}
}
func CreateUserInterface(name string, client *wgctrl.Client, backend backend.Backend, args *args.Args) (Interface, error) {
var err error
logger := log.WithFields(log.Fields{
"intf": name,
"type": "user",
})
dev := &UserDevice{
log: newLogger(logger),
}
logger.Debug("Starting in-process wireguard-go interface")
// Create TUN device
dev.tun, err = tun.CreateTUN(name, device.DefaultMTU)
if err != nil {
return nil, fmt.Errorf("failed to create TUN device: %w", err)
}
// Fix interface name
realName, err := dev.tun.Name()
if err == nil && realName != name {
name = realName
}
// Open UAPI file (or use supplied fd)
fileUAPI, err := ipc.UAPIOpen(name)
if err != nil {
return nil, fmt.Errorf("UAPI listen error: %w", err)
}
var bind conn.Bind = nil
if bind == nil {
bind = conn.NewDefaultBind()
}
// Create new device
dev.userDevice = device.NewDevice(dev.tun, bind, dev.log)
logger.Debug("Device started")
// Open UApi socket
dev.userAPI, err = ipc.UAPIListen(name, fileUAPI)
if err != nil {
return nil, fmt.Errorf("failed to listen on UAPI socket: %w", err)
}
// Handle UApi requests
go dev.handleUserApi()
logger.Debug("UAPI listener started for interface")
// Connect to UAPI
wgDev, err := client.Device(name)
if err != nil {
return nil, err
}
dev.BaseInterface, err = NewInterface(wgDev, client, backend, args)
if err != nil {
return nil, err
}
return dev, nil
}

132
pkg/intf/watch.go Normal file
View File

@@ -0,0 +1,132 @@
package intf
import (
"fmt"
"os"
"path"
"strings"
log "github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
"github.com/fsnotify/fsnotify"
"github.com/vishvananda/netlink"
nl "riasc.eu/wice/pkg/netlink"
)
const (
wireguardSockDir = "/var/run/wireguard/"
InterfaceAdded InterfaceEventOp = iota
InterfaceDeleted
)
type InterfaceEventOp int
type InterfaceEvent struct {
Op InterfaceEventOp
Name string
}
func (ls InterfaceEventOp) String() string {
switch ls {
case InterfaceAdded:
return "added"
case InterfaceDeleted:
return "deleted"
default:
return ""
}
}
func (e InterfaceEvent) String() string {
return fmt.Sprintf("%s %s", e.Name, e.Op)
}
func WatchWireguardInterfaces() (chan InterfaceEvent, chan error, error) {
events := make(chan InterfaceEvent, 16)
errors := make(chan error, 16)
done := make(<-chan struct{})
// Watch kernel interfaces
chNl := make(chan netlink.LinkUpdate, 32)
err := netlink.LinkSubscribeWithOptions(chNl, done, netlink.LinkSubscribeOptions{
ErrorCallback: func(err error) {
errors <- err
},
})
if err != nil {
return nil, nil, fmt.Errorf("failed to subscribe to netlink link event group: %w", err)
}
// Watch userspace UAPI sockets
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, nil, fmt.Errorf("failed to create fsnotify watcher: %w", err)
}
if _, err := os.Stat(wireguardSockDir); !os.IsNotExist(err) {
err = watcher.Add(wireguardSockDir)
if err != nil {
return nil, nil, fmt.Errorf("failed to watch %s: %w", wireguardSockDir, err)
}
}
go func() {
for {
select {
// Netlink link updates
case lu := <-chNl:
log.WithField("update", lu).Trace("Received netlink link update")
if lu.Link.Type() != nl.LinkTypeWireguard {
continue
}
switch lu.Header.Type {
case unix.RTM_NEWLINK:
events <- InterfaceEvent{
Op: InterfaceAdded,
Name: lu.Attrs().Name,
}
case unix.RTM_DELLINK:
events <- InterfaceEvent{
Op: InterfaceDeleted,
Name: lu.Attrs().Name,
}
}
// Fsnotify events
case event := <-watcher.Events:
log.WithField("event", event).Trace("Received fsnotify event")
name := normalizeSocketName(event.Name)
if event.Op&fsnotify.Create == fsnotify.Create {
events <- InterfaceEvent{
Op: InterfaceAdded,
Name: name,
}
} else if event.Op&fsnotify.Remove == fsnotify.Remove {
events <- InterfaceEvent{
Op: InterfaceDeleted,
Name: name,
}
} else {
log.Warn("Unknown fsnotify event: %+v", event)
}
// Fsnotify errors
case errors <- <-watcher.Errors:
log.Trace("Error while watching for link changes")
}
}
}()
return events, errors, nil
}
func normalizeSocketName(name string) string {
name = path.Base(name)
return strings.TrimSuffix(name, ".sock")
}

20
pkg/netlink/netlink.go Normal file
View File

@@ -0,0 +1,20 @@
package netlink
import "github.com/vishvananda/netlink"
const (
LinkTypeWireguard = "wireguard"
)
// github.com/vishvananda/netlink does not come with the wireguard link type yet
type Wireguard struct {
netlink.LinkAttrs
}
func (wg *Wireguard) Attrs() *netlink.LinkAttrs {
return &wg.LinkAttrs
}
func (wg *Wireguard) Type() string {
return LinkTypeWireguard
}

View File

@@ -0,0 +1,37 @@
package netlink_test
import (
"os"
"testing"
"github.com/vishvananda/netlink"
nl "riasc.eu/wice/pkg/netlink"
)
func TestWireguardLink(t *testing.T) {
if os.Getuid() != 0 {
t.Skip()
}
l := &nl.Wireguard{
LinkAttrs: netlink.NewLinkAttrs(),
}
l.LinkAttrs.Name = "wg-test0"
err := netlink.LinkAdd(l)
if err != nil {
t.Errorf("failed to create Wireguard interface: %s", err)
}
l2, err := netlink.LinkByName("wg-test0")
if err != nil {
t.Errorf("failed to get link details: %s", err)
}
if l2.Type() != "wireguard" {
t.Fail()
}
if err := netlink.LinkDel(l); err != nil {
t.Errorf("failed to delete Wireguard device: %w", err)
}
}

93
pkg/proxy/ebpf.go Normal file
View File

@@ -0,0 +1,93 @@
// +build linux
package proxy
import (
"fmt"
"net"
"runtime"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/asm"
"github.com/pion/ice/v2"
icex "riasc.eu/wice/internal/ice"
netx "riasc.eu/wice/internal/net"
log "github.com/sirupsen/logrus"
)
type EBPFProxy struct {
BaseProxy
}
func CheckEBPFSupport() bool {
return runtime.GOOS == "linux"
}
func SetupEBPFMux(agentConfig *ice.AgentConfig, listenPort int) error {
addr := net.UDPAddr{
IP: net.IPv4zero,
Port: listenPort,
}
conn, err := netx.NewFilteredUDPConn(addr)
if err != nil {
return fmt.Errorf("failed to create filtered UDP connection: %w", err)
}
spec := ebpf.ProgramSpec{
Type: ebpf.SocketFilter,
License: "Apache-2.0",
Instructions: asm.Instructions{
asm.Mov.Reg(asm.R6, asm.R1), // LDABS requires ctx in R6
asm.LoadAbs(-0x100000+22, asm.Half),
asm.JNE.Imm(asm.R0, int32(listenPort), "skip"),
asm.LoadAbs(-0x100000+32, asm.Word),
asm.JNE.Imm(asm.R0, int32(StunMagicCookie), "skip"),
asm.Mov.Imm(asm.R0, -1).Sym("exit"),
asm.Return(),
asm.Mov.Imm(asm.R0, 0).Sym("skip"),
asm.Return(),
},
}
prog, err := ebpf.NewProgramWithOptions(&spec, ebpf.ProgramOptions{
LogLevel: 1, // TODO take configured log-level from args
})
if err != nil {
return fmt.Errorf("failed to create BPF program: %w", err)
}
err = conn.ApplyFilter(prog)
if err != nil {
return fmt.Errorf("failed to attach eBPF program to socket: %w", err)
}
agentConfig.UDPMux = icex.NewFilteredUDPMux(icex.FilteredUDPMuxParams{
Logger: log.WithField("logger", "ice-mux"),
Conn: conn,
})
return nil
}
func NewEBPFProxy(ident string, listenPort int, cb UpdateEndpointCb, conn net.Conn) (*EBPFProxy, error) {
rUDPAddr := conn.RemoteAddr().(*net.UDPAddr)
cb(rUDPAddr)
return &EBPFProxy{
BaseProxy: BaseProxy{
Ident: ident,
},
// Conn: conn,
}, nil
}
func (bpf *EBPFProxy) Close() error {
return nil
}
func (bpf *EBPFProxy) UpdateEndpoint(addr *net.UDPAddr) error {
return nil
}

254
pkg/proxy/nftables.go Normal file
View File

@@ -0,0 +1,254 @@
// +build linux
package proxy
import (
"fmt"
"net"
"runtime"
"github.com/google/nftables"
"github.com/google/nftables/binaryutil"
"github.com/google/nftables/expr"
log "github.com/sirupsen/logrus"
"github.com/vishvananda/netns"
"golang.org/x/sys/unix"
)
type NFTablesProxy struct {
BaseProxy
logger log.FieldLogger
NFConn *nftables.Conn
Conn net.Conn
}
func CheckNFTablesSupport() bool {
return runtime.GOOS == "linux"
}
func NewNFTablesProxy(ident string, listenPort int, cb UpdateEndpointCb, conn net.Conn) (*NFTablesProxy, error) {
ns, err := netns.Get()
if err != nil {
return nil, err
}
proxy := &NFTablesProxy{
BaseProxy: BaseProxy{
Ident: ident,
ListenPort: listenPort,
},
logger: log.WithFields(log.Fields{
"logger": "proxy",
"type": "nftables",
}),
NFConn: &nftables.Conn{
NetNS: int(ns),
},
Conn: conn,
}
proxy.logger.Infof("Network namespace: %s", ns)
proxy.setupTable()
// Update Wireguard peer endpoint
rAddr := proxy.Conn.RemoteAddr().(*net.UDPAddr)
if err = cb(rAddr); err != nil {
return nil, err
}
proxy.logger.Info("Configured stateless nftables port redirection")
return proxy, nil
}
func (p *NFTablesProxy) deleteTable() error {
tb := nftables.Table{
Name: "wice",
Family: nftables.TableFamilyINet,
}
p.NFConn.DelTable(&tb) // Delete any previous existing table
p.NFConn.Flush() // We dont care about errors here...
return nil
}
func (p *NFTablesProxy) tableName() string {
// pkSlug := base32.HexEncoding.EncodeToString(p.WGPeer.PublicKey[:16])
return fmt.Sprintf("wice-%s", p.Ident)
}
func (p *NFTablesProxy) setupTable() error {
// Delete any stale tables created by WICE
p.deleteTable()
lAddr := p.Conn.LocalAddr().(*net.UDPAddr)
rAddr := p.Conn.RemoteAddr().(*net.UDPAddr)
tb := nftables.Table{
Name: p.tableName(),
Family: nftables.TableFamilyINet,
}
p.NFConn.AddTable(&tb)
// Ingress
chIngress := nftables.Chain{
Name: "ingress",
Type: nftables.ChainTypeFilter,
Hooknum: nftables.ChainHookInput,
Priority: nftables.ChainPriorityRaw,
Table: &tb,
}
p.NFConn.AddChain(&chIngress)
// Match non-STUN UDP ingress traffic directed at the port of our local ICE candidate
// and redirect to the listen port of the Wireguard interface.
// STUN traffic will pass to the iceConn for keepalives and connection checks.
rDnat := nftables.Rule{
Table: &tb,
Chain: &chIngress,
}
p.NFConn.AddChain(&chIngress)
// meta l4proto udp
rDnat.Exprs = append(rDnat.Exprs, &expr.Meta{
Key: expr.MetaKeyL4PROTO,
Register: 1,
})
rDnat.Exprs = append(rDnat.Exprs, &expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: []byte{unix.IPPROTO_UDP},
})
// udp dport lAddr.Port
rDnat.Exprs = append(rDnat.Exprs, &expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseTransportHeader,
Offset: 2,
Len: 2,
})
rDnat.Exprs = append(rDnat.Exprs, &expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: binaryutil.BigEndian.PutUint16(uint16(lAddr.Port)),
})
// @th,96,32 != StunMagicCookie
rDnat.Exprs = append(rDnat.Exprs, &expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseTransportHeader,
Offset: 12,
Len: 4,
})
rDnat.Exprs = append(rDnat.Exprs, &expr.Cmp{
Op: expr.CmpOpNeq,
Register: 1,
Data: binaryutil.BigEndian.PutUint32(StunMagicCookie),
})
// notrack
rDnat.Exprs = append(rDnat.Exprs, &expr.Notrack{})
// udp dport set p.Device.ListenPort
rDnat.Exprs = append(rDnat.Exprs, &expr.Immediate{
Register: 1,
Data: binaryutil.BigEndian.PutUint16(uint16(p.ListenPort)),
})
rDnat.Exprs = append(rDnat.Exprs, &expr.Payload{
OperationType: expr.PayloadWrite,
SourceRegister: 1,
Base: expr.PayloadBaseTransportHeader,
Offset: 2,
Len: 2,
})
p.NFConn.AddRule(&rDnat)
// Egress
chEgress := nftables.Chain{
Name: "egress",
Type: nftables.ChainTypeFilter,
Hooknum: nftables.ChainHookOutput,
Priority: nftables.ChainPriorityRaw,
Table: &tb,
}
p.NFConn.AddChain(&chEgress)
// Perform SNAT to the source port of Wireguard UDP traffic to match port of our local ICE candidate
rSnat := nftables.Rule{
Table: &tb,
Chain: &chEgress,
}
// meta l4proto udp
rSnat.Exprs = append(rSnat.Exprs, &expr.Meta{
Key: expr.MetaKeyL4PROTO,
Register: 1,
})
rSnat.Exprs = append(rSnat.Exprs, &expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: []byte{unix.IPPROTO_UDP},
})
// udp sport p.ListenPort
rSnat.Exprs = append(rSnat.Exprs, &expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseTransportHeader,
Offset: 0,
Len: 2,
})
rSnat.Exprs = append(rSnat.Exprs, &expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: binaryutil.BigEndian.PutUint16(uint16(p.ListenPort)),
})
// udp dport rAddr.Port
rSnat.Exprs = append(rSnat.Exprs, &expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseTransportHeader,
Offset: 2,
Len: 2,
})
rSnat.Exprs = append(rSnat.Exprs, &expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: binaryutil.BigEndian.PutUint16(uint16(rAddr.Port)),
})
// notrack
rSnat.Exprs = append(rSnat.Exprs, &expr.Notrack{})
// udp sport set lAddr.Port
rSnat.Exprs = append(rSnat.Exprs, &expr.Immediate{
Register: 1,
Data: binaryutil.BigEndian.PutUint16(uint16(lAddr.Port)),
})
rSnat.Exprs = append(rSnat.Exprs, &expr.Payload{
OperationType: expr.PayloadWrite,
SourceRegister: 1,
Base: expr.PayloadBaseTransportHeader,
Offset: 0,
Len: 2,
})
p.NFConn.AddRule(&rSnat)
if err := p.NFConn.Flush(); err != nil {
return fmt.Errorf("failed setup nftables: %w", err)
}
return nil
}
func (p *NFTablesProxy) Close() error {
return p.deleteTable()
}
func (bpf *NFTablesProxy) UpdateEndpoint(addr *net.UDPAddr) error {
return nil
}

98
pkg/proxy/proxy.go Normal file
View File

@@ -0,0 +1,98 @@
package proxy
import (
"errors"
"io"
"net"
)
type ProxyType int
type UpdateEndpointCb func(addr *net.UDPAddr) error
const (
ProxyTypeInvalid ProxyType = iota
ProxyTypeAuto
ProxyTypeUser
ProxyTypeNFTables
ProxyTypeEBPF
StunMagicCookie uint32 = 0x2112A442
)
type Proxy interface {
io.Closer
UpdateEndpoint(addr *net.UDPAddr) error
}
type BaseProxy struct {
ListenPort int
Ident string
}
func ProxyTypeFromString(typ string) ProxyType {
switch typ {
case "auto":
return ProxyTypeAuto
case "user":
return ProxyTypeUser
case "nftables":
return ProxyTypeNFTables
case "ebpf":
return ProxyTypeEBPF
default:
return ProxyTypeInvalid
}
}
func (pt ProxyType) String() string {
switch pt {
case ProxyTypeAuto:
return "auto"
case ProxyTypeUser:
return "user"
case ProxyTypeNFTables:
return "nftables"
case ProxyTypeEBPF:
return "ebpf"
}
return "invalid"
}
func AutoProxy() ProxyType {
if CheckEBPFSupport() {
return ProxyTypeEBPF
} else if CheckNFTablesSupport() {
return ProxyTypeNFTables
} else {
return ProxyTypeUser
}
}
func NewProxy(pt ProxyType, ident string, listenPort int, cb UpdateEndpointCb, conn net.Conn) (Proxy, error) {
switch pt {
case ProxyTypeUser:
return NewUserProxy(ident, listenPort, cb, conn)
case ProxyTypeNFTables:
return NewNFTablesProxy(ident, listenPort, cb, conn)
case ProxyTypeEBPF:
return NewEBPFProxy(ident, listenPort, cb, conn)
}
return nil, errors.New("unknown proxy type")
}
func Type(p Proxy) ProxyType {
switch p.(type) {
case *NFTablesProxy:
return ProxyTypeNFTables
case *UserProxy:
return ProxyTypeUser
case *EBPFProxy:
return ProxyTypeEBPF
default:
return ProxyTypeInvalid
}
}

74
pkg/proxy/user.go Normal file
View File

@@ -0,0 +1,74 @@
package proxy
import (
"io"
"net"
log "github.com/sirupsen/logrus"
)
const (
maxSegmentSize = (1 << 16) - 1
)
type UserProxy struct {
BaseProxy
conn *net.UDPConn
logger log.FieldLogger
}
func NewUserProxy(ident string, listenPort int, cb UpdateEndpointCb, conn net.Conn) (*UserProxy, error) {
var err error
proxy := &UserProxy{
BaseProxy: BaseProxy{
Ident: ident,
},
logger: log.WithFields(log.Fields{
"logger": "proxy",
"type": "user",
}),
}
// Userspace proxying
rAddr := net.UDPAddr{
IP: nil, // localhost
Port: listenPort,
}
lAddr := net.UDPAddr{
IP: net.IPv6loopback,
Port: 0, // choose automatically
}
proxy.conn, err = net.DialUDP("udp", &lAddr, &rAddr)
if err != nil {
return nil, err
}
// Update Wireguard peer endpoint
addr := proxy.conn.LocalAddr().(*net.UDPAddr)
err = cb(addr)
if err != nil {
return nil, err
}
ingressBuf := make([]byte, maxSegmentSize)
egressBuf := make([]byte, maxSegmentSize)
// Bi-directional copy between ICE and loopback UDP sockets until proxy.conn is closed
go io.CopyBuffer(conn, proxy.conn, ingressBuf)
go io.CopyBuffer(proxy.conn, conn, egressBuf)
proxy.logger.Info("Setup user-space proxy")
return proxy, nil
}
func (p *UserProxy) Close() error {
return p.conn.Close()
}
func (bpf *UserProxy) UpdateEndpoint(addr *net.UDPAddr) error {
return nil
}

42
test/nat_test.go Normal file
View File

@@ -0,0 +1,42 @@
package main
import (
"net"
"github.com/stv0g/gont"
)
func main() {
n := gont.NewNetwork("test")
n.Reset()
sw1, _ := n.AddSwitch("sw1")
sw2, _ := n.AddSwitch("sw2")
sw3, _ := n.AddSwitch("sw3")
mask := net.IPv4Mask(255, 255, 255, 0)
h12, _ := n.AddHost("h12", net.IPv4(10, 0, 1, 1),
&gont.Interface{"eth0", net.IPv4(10, 0, 1, 2), mask, sw1})
h22, _ := n.AddHost("h22", net.IPv4(10, 0, 2, 1),
&gont.Interface{"eth0", net.IPv4(10, 0, 2, 2), mask, sw2})
n.AddHost("h23", net.IPv4(10, 0, 2, 1),
&gont.Interface{"eth0", net.IPv4(10, 0, 2, 3), mask, sw2})
h32, _ := n.AddHost("h32", net.IPv4(10, 0, 3, 1),
&gont.Interface{"eth0", net.IPv4(10, 0, 3, 2), mask, sw3})
n.AddNAT("n1", nil,
&gont.Interface{"nb", net.IPv4(10, 0, 1, 1), mask, sw1},
&gont.Interface{"sb", net.IPv4(10, 0, 2, 1), mask, sw2})
n.AddNAT("n2", nil,
&gont.Interface{"nb", net.IPv4(10, 0, 2, 10), mask, sw2},
&gont.Interface{"sb", net.IPv4(10, 0, 3, 1), mask, sw3})
h32.Ping(h12)
h22.Traceroute(h12)
}

109
test/simple_test.go Normal file
View File

@@ -0,0 +1,109 @@
package hornet_test
import (
"bufio"
"fmt"
"io"
"net"
"os/exec"
"syscall"
"testing"
"time"
log "github.com/sirupsen/logrus"
)
func Killall(cmds ...*exec.Cmd) error {
for _, cmd := range cmds {
err := cmd.Process.Signal(syscall.SIGTERM)
if err != nil {
return err
}
err = cmd.Wait()
if err != nil {
return err
}
}
return nil
}
func SlicePrefix(prefix string, stream *io.ReadCloser) {
scanner := bufio.NewScanner(*stream)
for scanner.Scan() {
fmt.Println(scanner.Text()) // Println will add back the final '\n'
}
if err := scanner.Err(); err != nil {
log.WithError(err).Error("Reading stream")
}
}
func RunWice(h *gont.Host, args ...string) (*exec.Cmd, error) {
cmd := append([]string{"../../../cmd/wice/main.go"}, args...)
w, stdout, stderr, err := h.GoRunAsync(cmd...)
if err != nil {
return nil, err
}
go SlicePrefix("Wice "+h.Name+": ", stdout)
go SlicePrefix("Wice "+h.Name+": ", stderr)
return w, nil
}
func ConfigureWireguard(h *gont.Host) error {
return nil
}
func TestWice(t *testing.T) {
n := gont.NewNetwork("test")
defer n.Close()
sw, err := n.AddSwitch("sw")
if err != nil {
t.Fail()
}
// h1, err := n.AddHost("h1", nil, &gont.Interface{"eth0", net.IPv4(10, 0, 0, 1), mask(), sw})
// if err != nil {
// t.Fail()
// }
// h2, err := n.AddHost("h2", nil, &gont.Interface{"eth0", net.IPv4(10, 0, 0, 2), mask(), sw})
// if err != nil {
// t.Fail()
// }
h3, err := n.AddHost("h3", nil, &gont.Interface{"eth0", net.IPv4(10, 0, 0, 3), mask(), sw})
if err != nil {
t.Fail()
}
b, stdout, stderr, err := h3.GoRunAsync("../../../cmd/wice-signal-http")
if err != nil {
t.Fail()
}
go SlicePrefix("Backend: ", stdout)
go SlicePrefix("Backend: ", stderr)
// w1, err := RunWice(h1)
// if err != nil {
// t.Fail()
// }
// w2, err := RunWice(h2)
// if err != nil {
// t.Fail()
// }
h3.Run("curl", "http://h3:8080/")
time.Sleep(2 * time.Second)
if err = Killall(b); err != nil {
t.Fail()
}
}