Merge pull request 'develop' (#3) from develop into master
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: #3
This commit is contained in:
JakeHillion 2020-11-08 14:30:46 +00:00
commit b8dfe35300
22 changed files with 1140 additions and 0 deletions

50
.drone.yml Normal file
View File

@ -0,0 +1,50 @@
kind: pipeline
type: docker
name: default
steps:
- name: install
image: golang:1.15
environment:
GOPROXY: http://10.20.0.25:3142|direct
volumes:
- name: cache
path: /go
commands:
- go test -i ./...
- name: test
image: golang:1.15
volumes:
- name: cache
path: /go
commands:
- go test ./...
- name: build (debian)
image: golang:1.15-buster
when:
event:
- push
volumes:
- name: cache
path: /go
commands:
- go build
- name: upload
image: minio/mc
when:
event:
- push
environment:
ACCESS_KEY: 001f185da4dcda3000000000d
SECRET_KEY:
from_secret: s3_secret_key
commands:
- mc alias set s3 http://10.20.0.25:3900 $${ACCESS_KEY} $${SECRET_KEY}
- mc cp mpbl3p s3/dissertation/binaries/debian/${DRONE_BRANCH}
volumes:
- name: cache
temp: {}

117
.gitignore vendored Normal file
View File

@ -0,0 +1,117 @@
config.ini
logs/
# Created by https://www.toptal.com/developers/gitignore/api/intellij+all,go
# Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all,go
### Go ###
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
### Go Patch ###
/vendor/
/Godeps/
### Intellij+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Intellij+all Patch ###
# Ignores the whole .idea folder and all .iml files
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
.idea/
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
# Sonarlint plugin
.idea/sonarlint
# End of https://www.toptal.com/developers/gitignore/api/intellij+all,go

15
Makefile Normal file
View File

@ -0,0 +1,15 @@
manual:
docker run --rm -v /tmp:/tmp -v ${PWD}:/app -w /app golang:1.15-buster go build -o /tmp/mpbl3p
rsync -p /tmp/mpbl3p 10.21.12.101:
rsync -p /tmp/mpbl3p 10.21.12.102:
manual-bsd:
GOOS=freebsd go build -o /tmp/mpbl3p
rsync -p /tmp/mpbl3p 10.21.12.121:
rsync -p /tmp/mpbl3p 10.21.12.122:
getlogs:
sftp 10.21.12.100:tcpdump.pcap logs/speedtest.pcap
sftp 10.21.12.101:tcpdump.pcap logs/remote.pcap
sftp 10.21.12.102:tcpdump.pcap logs/local.pcap
sftp 10.21.12.103:tcpdump.pcap logs/client.pcap

View File

@ -1,2 +1,18 @@
# A Multi-Path Bidirectional Layer 3 Proxy
## Setup Notes
### Linux
#### Policy Based Routing
ip route flush 11
ip route add table 11 to 1.1.1.0/24 dev eth1
ip rule add from 1.1.1.4 table 11 priority 11
ip route flush 10
ip route add table 10 to 1.1.1.0/24 dev eth2
ip rule add from 1.1.1.5 table 10 priority 10
#### ARP Flux
sysctl -w net.ipv4.conf.all.arp_announce=1
sysctl -w net.ipv4.conf.all.arp_ignore=2

73
config/builder.go Normal file
View File

@ -0,0 +1,73 @@
package config
import (
"fmt"
"mpbl3p/proxy"
"mpbl3p/tcp"
"mpbl3p/tun"
)
// TODO: Delete this code as soon as an alternative is available
type UselessMac struct{}
func (UselessMac) CodeLength() int {
return 0
}
func (UselessMac) Generate([]byte) []byte {
return nil
}
func (u UselessMac) Verify([]byte, []byte) error {
return nil
}
func (c Configuration) Build() (*proxy.Proxy, error) {
p := proxy.NewProxy(0)
p.Generator = UselessMac{}
if c.Host.InterfaceName == "" {
c.Host.InterfaceName = "nc%d"
}
ss, err := tun.NewTun(c.Host.InterfaceName, 1500)
if err != nil {
return nil, err
}
p.Source = ss
p.Sink = ss
for _, peer := range c.Peers {
switch peer.Method {
case "TCP":
err := buildTcp(p, peer)
if err != nil {
return nil, err
}
}
}
return p, nil
}
func buildTcp(p *proxy.Proxy, peer Peer) error {
if peer.RemoteHost != "" {
f, err := tcp.InitiateFlow(
fmt.Sprintf("%s:", peer.LocalHost),
fmt.Sprintf("%s:%d", peer.RemoteHost, peer.RemotePort),
)
p.AddConsumer(f)
p.AddProducer(f, UselessMac{})
return err
}
err := tcp.NewListener(p, fmt.Sprintf("%s:%d", peer.LocalHost, peer.LocalPort), UselessMac{})
if err != nil {
return err
}
return nil
}

34
config/config.go Normal file
View File

@ -0,0 +1,34 @@
package config
import "github.com/go-playground/validator/v10"
var v = validator.New()
type Configuration struct {
Host Host
Peers []Peer `validate:"dive"`
}
type Host struct {
PrivateKey string `validate:"required"`
InterfaceName string
}
type Peer struct {
PublicKey string `validate:"required"`
Method string `validate:"oneof=TCP"`
LocalHost string `validate:"omitempty,ip"`
LocalPort uint `validate:"max=65535"`
RemoteHost string `validate:"required_with=RemotePort,omitempty,fqdn|ip"`
RemotePort uint `validate:"required_with=RemoteHost,omitempty,max=65535"`
KeepAlive uint
Timeout uint
RetryWait uint
}
func (c Configuration) Validate() error {
return v.Struct(c)
}

42
config/loader.go Normal file
View File

@ -0,0 +1,42 @@
package config
import (
"gopkg.in/ini.v1"
)
func LoadConfig(path string) (c Configuration, err error) {
var file *ini.File
file, err = ini.LoadSources(ini.LoadOptions{
AllowShadows: true,
AllowNonUniqueSections: true,
}, path)
if err != nil {
return
}
if s := file.Section("Host"); s != nil {
err = s.MapTo(&c.Host)
if err != nil {
return
}
}
var sections []*ini.Section
sections, err = file.SectionsByName("Peer")
for _, s := range sections {
if s == nil {
continue
}
p := Peer{}
err = s.MapTo(&p)
if err != nil {
return
}
c.Peers = append(c.Peers, p)
}
err = c.Validate()
return
}

12
go.mod Normal file
View File

@ -0,0 +1,12 @@
module mpbl3p
go 1.15
require (
github.com/go-playground/assert/v2 v2.0.1
github.com/go-playground/validator/v10 v10.4.1
github.com/pkg/taptun v0.0.0-20160424131934-bbbd335672ab
github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/stretchr/testify v1.4.0
gopkg.in/ini.v1 v1.62.0
)

45
go.sum Normal file
View File

@ -0,0 +1,45 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/pkg/taptun v0.0.0-20160424131934-bbbd335672ab h1:dAXDRtXYxj4sTR5WeRuTFJGH18QMT6AUpUgRwedI6es=
github.com/pkg/taptun v0.0.0-20160424131934-bbbd335672ab/go.mod h1:N5a/Ll2ZNk5wjiLNW9LIiNtO9RNYcaYmcXSYKMYrlDg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

36
main.go Normal file
View File

@ -0,0 +1,36 @@
package main
import (
"log"
"mpbl3p/config"
"os"
"os/signal"
"syscall"
)
func main() {
log.SetFlags(log.Ldate | log.Ltime | log.Llongfile)
log.Println("loading config...")
c, err := config.LoadConfig("config.ini")
if err != nil {
panic(err)
}
log.Println("building config...")
p, err := c.Build()
if err != nil {
panic(err)
}
log.Println("starting...")
p.Start()
log.Println("running")
signals := make(chan os.Signal)
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
<-signals
}

67
mocks/conn.go Normal file
View File

@ -0,0 +1,67 @@
package mocks
import "time"
type MockPerfectBiConn struct {
directionA chan byte
directionB chan byte
}
func NewMockPerfectBiConn(bufSize int) MockPerfectBiConn {
return MockPerfectBiConn{
directionA: make(chan byte, bufSize),
directionB: make(chan byte, bufSize),
}
}
func (bc MockPerfectBiConn) SideA() MockPerfectConn {
return MockPerfectConn{inbound: bc.directionA, outbound: bc.directionB}
}
func (bc MockPerfectBiConn) SideB() MockPerfectConn {
return MockPerfectConn{inbound: bc.directionB, outbound: bc.directionA}
}
type MockPerfectConn struct {
inbound chan byte
outbound chan byte
}
func (c MockPerfectConn) SetWriteDeadline(time.Time) error {
return nil
}
func (c MockPerfectConn) Read(p []byte) (n int, err error) {
for i := range p {
if i == 0 {
p[i] = <-c.inbound
} else {
select {
case b := <-c.inbound:
p[i] = b
default:
return i, nil
}
}
}
return len(p), nil
}
func (c MockPerfectConn) Write(p []byte) (n int, err error) {
for _, b := range p {
c.outbound <- b
}
return len(p), nil
}
func (c MockPerfectConn) NonBlockingRead(p []byte) (n int, err error) {
for i := range p {
select {
case b := <-c.inbound:
p[i] = b
default:
return i, nil
}
}
return len(p), nil
}

20
mocks/mac.go Normal file
View File

@ -0,0 +1,20 @@
package mocks
import "mpbl3p/shared"
type AlmostUselessMac struct{}
func (AlmostUselessMac) CodeLength() int {
return 4
}
func (AlmostUselessMac) Generate([]byte) []byte {
return []byte{'a', 'b', 'c', 'd'}
}
func (u AlmostUselessMac) Verify(_, sum []byte) error {
if !(sum[0] == 'a' && sum[1] == 'b' && sum[2] == 'c' && sum[3] == 'd') {
return shared.ErrBadChecksum
}
return nil
}

11
proxy/mac.go Normal file
View File

@ -0,0 +1,11 @@
package proxy
type MacGenerator interface {
CodeLength() int
Generate([]byte) []byte
}
type MacVerifier interface {
CodeLength() int
Verify(data []byte, sum []byte) error
}

60
proxy/packet.go Normal file
View File

@ -0,0 +1,60 @@
package proxy
import (
"encoding/binary"
"time"
)
type Packet struct {
Data []byte
timestamp time.Time
}
// create a packet from the raw data of an IP packet
func NewPacket(data []byte) Packet {
return Packet{
Data: data,
timestamp: time.Now(),
}
}
// rebuild a packet from the wrapped format
func UnmarshalPacket(raw []byte, verifier MacVerifier) (Packet, error) {
// the MAC is the last N bytes
data := raw[:len(raw)-verifier.CodeLength()]
sum := raw[len(raw)-verifier.CodeLength():]
if err := verifier.Verify(data, sum); err != nil {
return Packet{}, err
}
p := Packet{
Data: data[:len(data)-8],
}
unixTime := int64(binary.LittleEndian.Uint64(data[len(data)-8:]))
p.timestamp = time.Unix(unixTime, 0)
return p, nil
}
// get the raw data of the IP packet
func (p Packet) Raw() []byte {
return p.Data
}
// produce the wrapped format of a packet
func (p Packet) Marshal(generator MacGenerator) []byte {
// length of data + length of timestamp (8 byte) + length of checksum
slice := make([]byte, len(p.Data)+8+generator.CodeLength())
copy(slice, p.Data)
unixTime := uint64(p.timestamp.Unix())
binary.LittleEndian.PutUint64(slice[len(p.Data):], unixTime)
mac := generator.Generate(slice)
copy(slice[len(p.Data)+8:], mac)
return slice
}

34
proxy/packet_test.go Normal file
View File

@ -0,0 +1,34 @@
package proxy
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"mpbl3p/mocks"
"testing"
)
func TestPacket_Marshal(t *testing.T) {
testContent := []byte("A test string is the content of this packet.")
testPacket := NewPacket(testContent)
testMac := mocks.AlmostUselessMac{}
t.Run("Length", func(t *testing.T) {
marshalled := testPacket.Marshal(testMac)
assert.Len(t, marshalled, len(testContent)+8+4)
})
}
func TestUnmarshalPacket(t *testing.T) {
testContent := []byte("A test string is the content of this packet.")
testPacket := NewPacket(testContent)
testMac := mocks.AlmostUselessMac{}
testMarshalled := testPacket.Marshal(testMac)
t.Run("Length", func(t *testing.T) {
p, err := UnmarshalPacket(testMarshalled, testMac)
require.Nil(t, err)
assert.Len(t, p.Raw(), len(testContent))
})
}

128
proxy/proxy.go Normal file
View File

@ -0,0 +1,128 @@
package proxy
import (
"log"
"time"
)
type Producer interface {
IsAlive() bool
Produce(MacVerifier) (Packet, error)
}
type Consumer interface {
IsAlive() bool
Consume(Packet, MacGenerator) error
}
type Reconnectable interface {
Reconnect() error
}
type Source interface {
Source() (Packet, error)
}
type Sink interface {
Sink(packet Packet) error
}
type Proxy struct {
Source Source
Sink Sink
Generator MacGenerator
proxyChan chan Packet
sinkChan chan Packet
}
func NewProxy(bufferSize int) *Proxy {
return &Proxy{
proxyChan: make(chan Packet, bufferSize),
sinkChan: make(chan Packet, bufferSize),
}
}
func (p Proxy) Start() {
go func() {
for {
if packet, err := p.Source.Source(); err != nil {
panic(err)
return
} else {
p.proxyChan <- packet
}
}
}()
go func() {
for {
packet := <-p.sinkChan
if err := p.Sink.Sink(packet); err != nil {
panic(err)
return
}
}
}()
}
func (p Proxy) AddConsumer(c Consumer) {
go func() {
_, reconnectable := c.(Reconnectable)
for once := true; reconnectable || once; once = false {
if reconnectable {
var err error
for once := true; err != nil || once; once = false {
log.Printf("attempting to connect `%v`\n", c)
err = c.(Reconnectable).Reconnect()
if !once {
time.Sleep(time.Second)
}
}
log.Printf("connected `%v`\n", c)
}
for c.IsAlive() {
if err := c.Consume(<-p.proxyChan, p.Generator); err != nil {
log.Println(err)
break
}
}
}
log.Printf("closed connection `%v`\n", c)
}()
}
func (p Proxy) AddProducer(pr Producer, v MacVerifier) {
go func() {
_, reconnectable := pr.(Reconnectable)
for once := true; reconnectable || once; once = false {
if reconnectable {
var err error
for once := true; err != nil || once; once = false {
log.Printf("attempting to connect `%v`\n", pr)
err = pr.(Reconnectable).Reconnect()
if !once {
time.Sleep(time.Second)
}
}
log.Printf("connected `%v`\n", pr)
}
for pr.IsAlive() {
if packet, err := pr.Produce(v); err != nil {
log.Println(err)
break
} else {
p.sinkChan <- packet
}
}
}
log.Printf("closed connection `%v`\n", pr)
}()
}

6
shared/errors.go Normal file
View File

@ -0,0 +1,6 @@
package shared
import "errors"
var ErrBadChecksum = errors.New("the packet had a bad checksum")
var ErrDeadConnection = errors.New("the connection is dead")

157
tcp/flow.go Normal file
View File

@ -0,0 +1,157 @@
package tcp
import (
"encoding/binary"
"errors"
"io"
"mpbl3p/proxy"
"mpbl3p/shared"
"net"
"sync"
"time"
)
var ErrNotEnoughBytes = errors.New("not enough bytes")
type Conn interface {
Read(b []byte) (n int, err error)
Write(b []byte) (n int, err error)
SetWriteDeadline(time.Time) error
}
type InitiatedFlow struct {
Local string
Remote string
mu sync.RWMutex
Flow
}
type Flow struct {
conn Conn
isAlive bool
}
func InitiateFlow(local, remote string) (*InitiatedFlow, error) {
f := InitiatedFlow{
Local: local,
Remote: remote,
}
return &f, nil
}
func (f *InitiatedFlow) Reconnect() error {
f.mu.Lock()
defer f.mu.Unlock()
if f.isAlive {
return nil
}
localAddr, err := net.ResolveTCPAddr("tcp", f.Local)
if err != nil {
return err
}
remoteAddr, err := net.ResolveTCPAddr("tcp", f.Remote)
if err != nil {
return err
}
conn, err := net.DialTCP("tcp", localAddr, remoteAddr)
if err != nil {
return err
}
err = conn.SetWriteBuffer(0)
if err != nil {
return err
}
f.conn = conn
f.isAlive = true
return nil
}
func (f *Flow) IsAlive() bool {
return f.isAlive
}
func (f *InitiatedFlow) Consume(p proxy.Packet, g proxy.MacGenerator) error {
f.mu.RLock()
defer f.mu.RUnlock()
return f.Flow.Consume(p, g)
}
func (f *Flow) Consume(p proxy.Packet, g proxy.MacGenerator) (err error) {
if !f.isAlive {
return shared.ErrDeadConnection
}
data := p.Marshal(g)
err = f.consumeMarshalled(data)
if err != nil {
f.isAlive = false
}
return
}
func (f *Flow) consumeMarshalled(data []byte) error {
prefixedData := make([]byte, len(data)+4)
binary.LittleEndian.PutUint32(prefixedData, uint32(len(data)))
copy(prefixedData[4:], data)
err := f.conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
if err != nil {
return err
}
_, err = f.conn.Write(prefixedData)
return err
}
func (f *InitiatedFlow) Produce(v proxy.MacVerifier) (proxy.Packet, error) {
f.mu.RLock()
defer f.mu.RUnlock()
return f.Flow.Produce(v)
}
func (f *Flow) Produce(v proxy.MacVerifier) (proxy.Packet, error) {
if !f.isAlive {
return proxy.Packet{}, shared.ErrDeadConnection
}
data, err := f.produceMarshalled()
if err != nil {
f.isAlive = false
return proxy.Packet{}, err
}
return proxy.UnmarshalPacket(data, v)
}
func (f *Flow) produceMarshalled() ([]byte, error) {
lengthBytes := make([]byte, 4)
if n, err := io.LimitReader(f.conn, 4).Read(lengthBytes); err != nil {
return nil, err
} else if n != 4 {
return nil, ErrNotEnoughBytes
}
length := binary.LittleEndian.Uint32(lengthBytes)
dataBytes := make([]byte, length)
var read uint32
for read < length {
if n, err := io.LimitReader(f.conn, int64(length-read)).Read(dataBytes[read:]); err != nil {
return nil, err
} else {
read += uint32(n)
}
}
return dataBytes, nil
}

66
tcp/flow_test.go Normal file
View File

@ -0,0 +1,66 @@
package tcp
import (
"encoding/binary"
"github.com/go-playground/assert/v2"
"github.com/stretchr/testify/require"
"mpbl3p/mocks"
"mpbl3p/proxy"
"testing"
)
func TestFlow_Consume(t *testing.T) {
testContent := []byte("A test string is the content of this packet.")
testPacket := proxy.NewPacket(testContent)
testMac := mocks.AlmostUselessMac{}
t.Run("Length", func(t *testing.T) {
testConn := mocks.NewMockPerfectBiConn(100)
flowA := Flow{conn: testConn.SideA(), isAlive: true}
err := flowA.Consume(testPacket, testMac)
require.Nil(t, err)
buf := make([]byte, 100)
n, err := testConn.SideB().Read(buf)
require.Nil(t, err)
assert.Equal(t, len(testContent)+8+4+4, n)
assert.Equal(t, uint32(len(testContent)+8+4), binary.LittleEndian.Uint32(buf[:len(buf)-4]))
})
}
func TestFlow_Produce(t *testing.T) {
testContent := "A test string is the content of this packet."
testMarshalled := []byte("0000" + testContent + "00000000abcd")
binary.LittleEndian.PutUint32(testMarshalled, uint32(len(testMarshalled)-4))
testMac := mocks.AlmostUselessMac{}
t.Run("Length", func(t *testing.T) {
testConn := mocks.NewMockPerfectBiConn(100)
flowA := Flow{conn: testConn.SideA(), isAlive: true}
_, err := testConn.SideB().Write(testMarshalled)
require.Nil(t, err)
p, err := flowA.Produce(testMac)
require.Nil(t, err)
assert.Equal(t, len(testContent), len(p.Raw()))
})
t.Run("Value", func(t *testing.T) {
testConn := mocks.NewMockPerfectBiConn(100)
flowA := Flow{conn: testConn.SideA(), isAlive: true}
_, err := testConn.SideB().Write(testMarshalled)
require.Nil(t, err)
p, err := flowA.Produce(testMac)
require.Nil(t, err)
assert.Equal(t, testContent, string(p.Raw()))
})
}

42
tcp/listener.go Normal file
View File

@ -0,0 +1,42 @@
package tcp
import (
"log"
"mpbl3p/proxy"
"net"
)
func NewListener(p *proxy.Proxy, local string, v proxy.MacVerifier) error {
laddr, err := net.ResolveTCPAddr("tcp", local)
if err != nil {
return err
}
listener, err := net.ListenTCP("tcp", laddr)
if err != nil {
return err
}
go func() {
for {
conn, err := listener.AcceptTCP()
if err != nil {
panic(err)
}
err = conn.SetWriteBuffer(0)
if err != nil {
panic(err)
}
f := Flow{conn: conn, isAlive: true}
log.Printf("received new connection: %v\n", f)
p.AddConsumer(&f)
p.AddProducer(&f, v)
}
}()
return nil
}

96
tun/tun.go Normal file
View File

@ -0,0 +1,96 @@
package tun
import (
"github.com/pkg/taptun"
"io"
"log"
"mpbl3p/proxy"
"net"
"os"
"strings"
"sync"
"time"
)
type SourceSink struct {
tun *taptun.Tun
bufferSize int
up bool
upMu sync.Mutex
}
func NewTun(namingScheme string, bufferSize int) (ss *SourceSink, err error) {
ss = &SourceSink{}
ss.tun, err = taptun.NewTun(namingScheme)
if err != nil {
return
}
ss.bufferSize = bufferSize
ss.upMu.Lock()
go func() {
defer ss.upMu.Unlock()
for {
iface, err := net.InterfaceByName(ss.tun.String())
if err != nil {
panic(err)
}
if strings.Contains(iface.Flags.String(), "up") {
log.Println("tun is up")
ss.up = true
return
}
time.Sleep(100 * time.Millisecond)
}
}()
return
}
func (t *SourceSink) Source() (proxy.Packet, error) {
if !t.up {
t.upMu.Lock()
t.upMu.Unlock()
}
buf := make([]byte, t.bufferSize)
read, err := t.tun.Read(buf)
if err != nil {
return proxy.Packet{}, err
}
if read == 0 {
return proxy.Packet{}, io.EOF
}
return proxy.NewPacket(buf[:read]), nil
}
var good, bad float64
func (t *SourceSink) Sink(packet proxy.Packet) error {
if !t.up {
t.upMu.Lock()
t.upMu.Unlock()
}
_, err := t.tun.Write(packet.Raw())
if err != nil {
switch err.(type) {
case *os.PathError:
bad += 1
log.Printf("packet loss: %f%%\n", bad*100/(good+bad))
return nil
default:
return err
}
}
good += 1
return nil
}

13
utils/utils.go Normal file
View File

@ -0,0 +1,13 @@
package utils
var NextId = make(chan int)
func init() {
go func() {
i := 0
for {
NextId <- i
i += 1
}
}()
}