wip
This commit is contained in:
parent
55a9bf9dc3
commit
03ff1aac80
2
.gitignore
vendored
2
.gitignore
vendored
@ -20,4 +20,4 @@
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
test-scripts
|
||||
|
86
README.md
86
README.md
@ -1,2 +1,86 @@
|
||||
# vppn
|
||||
# vppn: Virtual Pretty Private Network
|
||||
|
||||
## Roadmap
|
||||
|
||||
* Peer: router: create process for managing the routing table
|
||||
* Peer: router: track mediators, enable / disable ...
|
||||
* Hub: track peer last-seen timestamp
|
||||
* Peer: local peer discovery - part of RoutingProcessor
|
||||
|
||||
## Principles
|
||||
|
||||
* Creates an IPv4/24 network with a maximum of 254 peers. (1-254)
|
||||
* Simple setup: via setup link from the hub.
|
||||
* Each peer has full network state replicated from the hub.
|
||||
|
||||
## Design
|
||||
|
||||
* Append nonce to end of packet
|
||||
* Then it's readable whether signed or unsiged
|
||||
* Types of packets to send:
|
||||
* standard: encrypt and send
|
||||
* Forward via: encrypt, sign and send
|
||||
* Forward to: send
|
||||
* Type of packeting read from interface:
|
||||
* Forward to: check signature
|
||||
* Forwarded, standard
|
||||
|
||||
Incoming from net:
|
||||
* Data for iface
|
||||
* Packet for forward
|
||||
* Packet for routingHandler
|
||||
* Incoming from iface:
|
||||
* Data for peer
|
||||
|
||||
## Hub Server Configuration
|
||||
|
||||
```
|
||||
# Create user.
|
||||
adduser user
|
||||
|
||||
# Enable ssh.
|
||||
cp -r ~/.ssh /home/user/
|
||||
chown -R user:user /home/user/.ssh
|
||||
|
||||
```
|
||||
|
||||
Upload `hub` executable:
|
||||
|
||||
```
|
||||
scp hub user@<remote>:~/
|
||||
```
|
||||
|
||||
Create systemd file in `/etc/systemd/system/hub.service
|
||||
|
||||
```
|
||||
Description=hub
|
||||
Requires=network.target
|
||||
|
||||
[Service]
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
Type=simple
|
||||
User=user
|
||||
WorkingDirectory=/home/user/
|
||||
ExecStart=/home/user/hub -listen <addr>:https -secure=true -root-dir=/home/user
|
||||
Restart=always
|
||||
RestartSec=8
|
||||
TimeoutStopSec=24
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
```
|
||||
|
||||
Add and start the hub server:
|
||||
|
||||
```
|
||||
systemctl daemon-reload
|
||||
systemctl start hub
|
||||
```
|
||||
|
||||
Get initial password from logs:
|
||||
|
||||
```
|
||||
journalctl -f -u hub -n 100
|
||||
```
|
||||
|
||||
Sign-in and configure.
|
||||
|
7
cmd/hub/main.go
Normal file
7
cmd/hub/main.go
Normal file
@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "vppn/hub"
|
||||
|
||||
func main() {
|
||||
hub.Main()
|
||||
}
|
4
cmd/vppn/build.sh
Executable file
4
cmd/vppn/build.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
go build
|
||||
sudo setcap cap_net_admin+iep vppn
|
11
cmd/vppn/main.go
Normal file
11
cmd/vppn/main.go
Normal file
@ -0,0 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"vppn/peer"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
peer.Main()
|
||||
}
|
20
fasttime/time.go
Normal file
20
fasttime/time.go
Normal file
@ -0,0 +1,20 @@
|
||||
package fasttime
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
var _timestamp int64 = time.Now().Unix()
|
||||
|
||||
func init() {
|
||||
go func() {
|
||||
for range time.Tick(1100 * time.Millisecond) {
|
||||
atomic.StoreInt64(&_timestamp, time.Now().Unix())
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func Now() int64 {
|
||||
return atomic.LoadInt64(&_timestamp)
|
||||
}
|
18
fasttime/time_test.go
Normal file
18
fasttime/time_test.go
Normal file
@ -0,0 +1,18 @@
|
||||
package fasttime
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func BenchmarkNow(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
Now()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTimeUnix(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
time.Now().Unix()
|
||||
}
|
||||
}
|
17
go.mod
Normal file
17
go.mod
Normal file
@ -0,0 +1,17 @@
|
||||
module vppn
|
||||
|
||||
go 1.23.2
|
||||
|
||||
require (
|
||||
git.crumpington.com/lib/go v0.8.1
|
||||
git.crumpington.com/lib/webutil v0.0.7
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
|
||||
golang.org/x/crypto v0.29.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||
golang.org/x/net v0.31.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
)
|
16
go.sum
Normal file
16
go.sum
Normal file
@ -0,0 +1,16 @@
|
||||
git.crumpington.com/lib/go v0.8.1 h1:rWjddllSxQ4yReraqDaGZAod4NpRD9LtGx1yV71ytcU=
|
||||
git.crumpington.com/lib/go v0.8.1/go.mod h1:XjQaf2NFlje9BJ1EevZL8NNioPrAe7WwHpKUhcDw2Lk=
|
||||
git.crumpington.com/lib/webutil v0.0.7 h1:1RG9CpuXYalT0NPj8fvxjOLV566LqL37APvAdASFzgA=
|
||||
git.crumpington.com/lib/webutil v0.0.7/go.mod h1:efIEiuK1uqFIhI/dlsWUHMsC5bXcEbJEjmdluRoFPPQ=
|
||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
260
hub/api/api.go
Normal file
260
hub/api/api.go
Normal file
@ -0,0 +1,260 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"embed"
|
||||
"errors"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
"vppn/hub/api/db"
|
||||
"vppn/m"
|
||||
|
||||
"git.crumpington.com/lib/go/idgen"
|
||||
"git.crumpington.com/lib/go/sqliteutil"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/crypto/nacl/sign"
|
||||
)
|
||||
|
||||
//go:embed migrations
|
||||
var migrations embed.FS
|
||||
|
||||
type API struct {
|
||||
db *sql.DB
|
||||
lock sync.Mutex
|
||||
peerIntents map[string]PeerCreateArgs
|
||||
}
|
||||
|
||||
func New(dbPath string) (*API, error) {
|
||||
sqlDB, err := sql.Open("sqlite3", dbPath+"?_journal=WAL")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := sqliteutil.Migrate(sqlDB, migrations); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a := &API{
|
||||
db: sqlDB,
|
||||
peerIntents: map[string]PeerCreateArgs{},
|
||||
}
|
||||
|
||||
return a, a.ensurePassword()
|
||||
}
|
||||
|
||||
func (a *API) ensurePassword() error {
|
||||
_, err := db.Config_Get(a.db, 1)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
|
||||
pwd := idgen.NewToken()
|
||||
|
||||
log.Printf("Setting password: %s", pwd)
|
||||
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conf := &Config{
|
||||
ConfigID: 1,
|
||||
VPNNetwork: []byte{10, 1, 1, 0},
|
||||
Password: hashed,
|
||||
}
|
||||
|
||||
return db.Config_Insert(a.db, conf)
|
||||
}
|
||||
|
||||
func (a *API) Config_Get() *Config {
|
||||
conf, err := db.Config_Get(a.db, 1)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return conf
|
||||
}
|
||||
|
||||
func (a *API) Config_Update(conf *Config) error {
|
||||
return db.Config_Update(a.db, conf)
|
||||
}
|
||||
|
||||
func (a *API) Config_UpdatePassword(pwdHash []byte) error {
|
||||
return db.Config_UpdatePassword(a.db, pwdHash)
|
||||
}
|
||||
|
||||
func (a *API) Session_Delete(sessionID string) error {
|
||||
return db.Session_Delete(a.db, sessionID)
|
||||
}
|
||||
|
||||
func (a *API) Session_Get(sessionID string) (*Session, error) {
|
||||
if sessionID == "" {
|
||||
return a.session_CreatePub()
|
||||
}
|
||||
|
||||
session, err := db.Session_Get(a.db, sessionID)
|
||||
if err != nil {
|
||||
return a.session_CreatePub()
|
||||
}
|
||||
|
||||
if timeSince(session.LastSeenAt) > 86400*21 {
|
||||
return a.session_CreatePub()
|
||||
}
|
||||
|
||||
if timeSince(session.LastSeenAt) > 86400*7 {
|
||||
session.LastSeenAt = time.Now().Unix()
|
||||
if err := db.Session_UpdateLastSeenAt(a.db, session.SessionID); err != nil {
|
||||
log.Printf("Failed to update session: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (a *API) session_CreatePub() (*Session, error) {
|
||||
s := &Session{
|
||||
SessionID: idgen.NewToken(),
|
||||
CSRF: idgen.NewToken(),
|
||||
SignedIn: false,
|
||||
CreatedAt: time.Now().Unix(),
|
||||
LastSeenAt: time.Now().Unix(),
|
||||
}
|
||||
err := db.Session_Insert(a.db, s)
|
||||
return s, err
|
||||
}
|
||||
|
||||
func (a *API) Session_DeleteBefore(timestamp int64) error {
|
||||
return db.Session_DeleteBefore(a.db, timestamp)
|
||||
}
|
||||
|
||||
func (a *API) Session_SignIn(s *Session, pwd string) error {
|
||||
conf := a.Config_Get()
|
||||
if err := bcrypt.CompareHashAndPassword(conf.Password, []byte(pwd)); err != nil {
|
||||
return ErrNotAuthorized
|
||||
}
|
||||
|
||||
return db.Session_SetSignedIn(a.db, s.SessionID)
|
||||
}
|
||||
|
||||
type PeerCreateArgs struct {
|
||||
Name string
|
||||
IP []byte
|
||||
Port uint16
|
||||
Mediator bool
|
||||
}
|
||||
|
||||
// Create the intention to add a peer. The returned code is used to complete
|
||||
// the peer creation. The code is valid for 5 minutes.
|
||||
func (a *API) Peer_CreateIntent(args PeerCreateArgs) string {
|
||||
a.lock.Lock()
|
||||
defer a.lock.Unlock()
|
||||
|
||||
code := idgen.NewToken()
|
||||
a.peerIntents[code] = args
|
||||
|
||||
go func() {
|
||||
time.Sleep(5 * time.Minute)
|
||||
a.lock.Lock()
|
||||
defer a.lock.Unlock()
|
||||
delete(a.peerIntents, code)
|
||||
}()
|
||||
|
||||
return code
|
||||
}
|
||||
|
||||
func (a *API) Peer_Create(creationCode string) (*m.PeerConfig, error) {
|
||||
a.lock.Lock()
|
||||
defer a.lock.Unlock()
|
||||
|
||||
args, ok := a.peerIntents[creationCode]
|
||||
if !ok {
|
||||
return nil, ErrNotAuthorized
|
||||
}
|
||||
|
||||
delete(a.peerIntents, creationCode)
|
||||
|
||||
encPubKey, encPrivKey, err := box.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signPubKey, signPrivKey, err := sign.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get peer IP.
|
||||
peerIP := byte(0)
|
||||
|
||||
for i := byte(1); i < 255; i++ {
|
||||
exists, err := db.Peer_Exists(a.db, i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
peerIP = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if peerIP == 0 {
|
||||
return nil, ErrNoIPAvailable
|
||||
}
|
||||
|
||||
peer := &Peer{
|
||||
PeerIP: peerIP,
|
||||
APIKey: idgen.NewToken(),
|
||||
Name: args.Name,
|
||||
IP: args.IP,
|
||||
Port: args.Port,
|
||||
Mediator: args.Mediator,
|
||||
EncPubKey: encPubKey[:],
|
||||
SignPubKey: signPubKey[:],
|
||||
}
|
||||
|
||||
if err := db.Peer_Insert(a.db, peer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf := a.Config_Get()
|
||||
|
||||
return &m.PeerConfig{
|
||||
PeerIP: peer.PeerIP,
|
||||
HubAddress: conf.HubAddress,
|
||||
APIKey: peer.APIKey,
|
||||
Network: conf.VPNNetwork,
|
||||
IP: peer.IP,
|
||||
Port: peer.Port,
|
||||
Mediator: peer.Mediator,
|
||||
EncPubKey: encPubKey[:],
|
||||
EncPrivKey: encPrivKey[:],
|
||||
SignPubKey: signPubKey[:],
|
||||
SignPrivKey: signPrivKey[:],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *API) Peer_Update(p *Peer) error {
|
||||
return db.Peer_Update(a.db, p)
|
||||
}
|
||||
|
||||
func (a *API) Peer_Delete(ip byte) error {
|
||||
return db.Peer_Delete(a.db, ip)
|
||||
}
|
||||
|
||||
func (a *API) Peer_List() ([]*Peer, error) {
|
||||
return db.Peer_ListAll(a.db)
|
||||
}
|
||||
|
||||
func (a *API) Peer_Get(ip byte) (*Peer, error) {
|
||||
return db.Peer_Get(a.db, ip)
|
||||
}
|
||||
|
||||
func (a *API) Peer_GetByAPIKey(key string) (*Peer, error) {
|
||||
return db.Peer_GetByAPIKey(a.db, key)
|
||||
}
|
3
hub/api/db/generate.go
Normal file
3
hub/api/db/generate.go
Normal file
@ -0,0 +1,3 @@
|
||||
package db
|
||||
|
||||
//go:generate sqlgen sqlite tables.defs generated.go
|
0
hub/api/db/generated
Normal file
0
hub/api/db/generated
Normal file
480
hub/api/db/generated.go
Normal file
480
hub/api/db/generated.go
Normal file
@ -0,0 +1,480 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"iter"
|
||||
)
|
||||
|
||||
type TX interface {
|
||||
Exec(query string, args ...any) (sql.Result, error)
|
||||
Query(query string, args ...any) (*sql.Rows, error)
|
||||
QueryRow(query string, args ...any) *sql.Row
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Table: config
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type Config struct {
|
||||
ConfigID int64
|
||||
HubAddress string
|
||||
VPNNetwork []byte
|
||||
Password []byte
|
||||
}
|
||||
|
||||
const Config_SelectQuery = "SELECT ConfigID,HubAddress,VPNNetwork,Password FROM config"
|
||||
|
||||
func Config_Insert(
|
||||
tx TX,
|
||||
row *Config,
|
||||
) (err error) {
|
||||
Config_Sanitize(row)
|
||||
if err = Config_Validate(row); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec("INSERT INTO config(ConfigID,HubAddress,VPNNetwork,Password) VALUES(?,?,?,?)", row.ConfigID, row.HubAddress, row.VPNNetwork, row.Password)
|
||||
return err
|
||||
}
|
||||
|
||||
func Config_Update(
|
||||
tx TX,
|
||||
row *Config,
|
||||
) (err error) {
|
||||
Config_Sanitize(row)
|
||||
if err = Config_Validate(row); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := tx.Exec("UPDATE config SET HubAddress=?,VPNNetwork=? WHERE ConfigID=?", row.HubAddress, row.VPNNetwork, row.ConfigID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
switch n {
|
||||
case 0:
|
||||
return sql.ErrNoRows
|
||||
case 1:
|
||||
return nil
|
||||
default:
|
||||
panic("multiple rows updated")
|
||||
}
|
||||
}
|
||||
|
||||
func Config_UpdateFull(
|
||||
tx TX,
|
||||
row *Config,
|
||||
) (err error) {
|
||||
Config_Sanitize(row)
|
||||
if err = Config_Validate(row); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := tx.Exec("UPDATE config SET HubAddress=?,VPNNetwork=?,Password=? WHERE ConfigID=?", row.HubAddress, row.VPNNetwork, row.Password, row.ConfigID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
switch n {
|
||||
case 0:
|
||||
return sql.ErrNoRows
|
||||
case 1:
|
||||
return nil
|
||||
default:
|
||||
panic("multiple rows updated")
|
||||
}
|
||||
}
|
||||
|
||||
func Config_Delete(
|
||||
tx TX,
|
||||
ConfigID int64,
|
||||
) (err error) {
|
||||
result, err := tx.Exec("DELETE FROM config WHERE ConfigID=?", ConfigID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
switch n {
|
||||
case 0:
|
||||
return sql.ErrNoRows
|
||||
case 1:
|
||||
return nil
|
||||
default:
|
||||
panic("multiple rows deleted")
|
||||
}
|
||||
}
|
||||
|
||||
func Config_Get(
|
||||
tx TX,
|
||||
ConfigID int64,
|
||||
) (
|
||||
row *Config,
|
||||
err error,
|
||||
) {
|
||||
row = &Config{}
|
||||
r := tx.QueryRow("SELECT ConfigID,HubAddress,VPNNetwork,Password FROM config WHERE ConfigID=?", ConfigID)
|
||||
err = r.Scan(&row.ConfigID, &row.HubAddress, &row.VPNNetwork, &row.Password)
|
||||
return
|
||||
}
|
||||
|
||||
func Config_GetWhere(
|
||||
tx TX,
|
||||
query string,
|
||||
args ...any,
|
||||
) (
|
||||
row *Config,
|
||||
err error,
|
||||
) {
|
||||
row = &Config{}
|
||||
r := tx.QueryRow(query, args...)
|
||||
err = r.Scan(&row.ConfigID, &row.HubAddress, &row.VPNNetwork, &row.Password)
|
||||
return
|
||||
}
|
||||
|
||||
func Config_Iterate(
|
||||
tx TX,
|
||||
query string,
|
||||
args ...any,
|
||||
) iter.Seq2[*Config, error] {
|
||||
rows, err := tx.Query(query, args...)
|
||||
if err != nil {
|
||||
return func(yield func(*Config, error) bool) {
|
||||
yield(nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
return func(yield func(*Config, error) bool) {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
row := &Config{}
|
||||
err := rows.Scan(&row.ConfigID, &row.HubAddress, &row.VPNNetwork, &row.Password)
|
||||
if !yield(row, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Config_List(
|
||||
tx TX,
|
||||
query string,
|
||||
args ...any,
|
||||
) (
|
||||
l []*Config,
|
||||
err error,
|
||||
) {
|
||||
for row, err := range Config_Iterate(tx, query, args...) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l = append(l, row)
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Table: sessions
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type Session struct {
|
||||
SessionID string
|
||||
CSRF string
|
||||
SignedIn bool
|
||||
CreatedAt int64
|
||||
LastSeenAt int64
|
||||
}
|
||||
|
||||
const Session_SelectQuery = "SELECT SessionID,CSRF,SignedIn,CreatedAt,LastSeenAt FROM sessions"
|
||||
|
||||
func Session_Insert(
|
||||
tx TX,
|
||||
row *Session,
|
||||
) (err error) {
|
||||
Session_Sanitize(row)
|
||||
if err = Session_Validate(row); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec("INSERT INTO sessions(SessionID,CSRF,SignedIn,CreatedAt,LastSeenAt) VALUES(?,?,?,?,?)", row.SessionID, row.CSRF, row.SignedIn, row.CreatedAt, row.LastSeenAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func Session_Delete(
|
||||
tx TX,
|
||||
SessionID string,
|
||||
) (err error) {
|
||||
result, err := tx.Exec("DELETE FROM sessions WHERE SessionID=?", SessionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
switch n {
|
||||
case 0:
|
||||
return sql.ErrNoRows
|
||||
case 1:
|
||||
return nil
|
||||
default:
|
||||
panic("multiple rows deleted")
|
||||
}
|
||||
}
|
||||
|
||||
func Session_Get(
|
||||
tx TX,
|
||||
SessionID string,
|
||||
) (
|
||||
row *Session,
|
||||
err error,
|
||||
) {
|
||||
row = &Session{}
|
||||
r := tx.QueryRow("SELECT SessionID,CSRF,SignedIn,CreatedAt,LastSeenAt FROM sessions WHERE SessionID=?", SessionID)
|
||||
err = r.Scan(&row.SessionID, &row.CSRF, &row.SignedIn, &row.CreatedAt, &row.LastSeenAt)
|
||||
return
|
||||
}
|
||||
|
||||
func Session_GetWhere(
|
||||
tx TX,
|
||||
query string,
|
||||
args ...any,
|
||||
) (
|
||||
row *Session,
|
||||
err error,
|
||||
) {
|
||||
row = &Session{}
|
||||
r := tx.QueryRow(query, args...)
|
||||
err = r.Scan(&row.SessionID, &row.CSRF, &row.SignedIn, &row.CreatedAt, &row.LastSeenAt)
|
||||
return
|
||||
}
|
||||
|
||||
func Session_Iterate(
|
||||
tx TX,
|
||||
query string,
|
||||
args ...any,
|
||||
) iter.Seq2[*Session, error] {
|
||||
rows, err := tx.Query(query, args...)
|
||||
if err != nil {
|
||||
return func(yield func(*Session, error) bool) {
|
||||
yield(nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
return func(yield func(*Session, error) bool) {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
row := &Session{}
|
||||
err := rows.Scan(&row.SessionID, &row.CSRF, &row.SignedIn, &row.CreatedAt, &row.LastSeenAt)
|
||||
if !yield(row, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Session_List(
|
||||
tx TX,
|
||||
query string,
|
||||
args ...any,
|
||||
) (
|
||||
l []*Session,
|
||||
err error,
|
||||
) {
|
||||
for row, err := range Session_Iterate(tx, query, args...) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l = append(l, row)
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Table: peers
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type Peer struct {
|
||||
PeerIP byte
|
||||
APIKey string
|
||||
Name string
|
||||
IP []byte
|
||||
Port uint16
|
||||
Mediator bool
|
||||
EncPubKey []byte
|
||||
SignPubKey []byte
|
||||
}
|
||||
|
||||
const Peer_SelectQuery = "SELECT PeerIP,APIKey,Name,IP,Port,Mediator,EncPubKey,SignPubKey FROM peers"
|
||||
|
||||
func Peer_Insert(
|
||||
tx TX,
|
||||
row *Peer,
|
||||
) (err error) {
|
||||
Peer_Sanitize(row)
|
||||
if err = Peer_Validate(row); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec("INSERT INTO peers(PeerIP,APIKey,Name,IP,Port,Mediator,EncPubKey,SignPubKey) VALUES(?,?,?,?,?,?,?,?)", row.PeerIP, row.APIKey, row.Name, row.IP, row.Port, row.Mediator, row.EncPubKey, row.SignPubKey)
|
||||
return err
|
||||
}
|
||||
|
||||
func Peer_Update(
|
||||
tx TX,
|
||||
row *Peer,
|
||||
) (err error) {
|
||||
Peer_Sanitize(row)
|
||||
if err = Peer_Validate(row); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := tx.Exec("UPDATE peers SET Name=?,IP=?,Port=?,Mediator=? WHERE PeerIP=?", row.Name, row.IP, row.Port, row.Mediator, row.PeerIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
switch n {
|
||||
case 0:
|
||||
return sql.ErrNoRows
|
||||
case 1:
|
||||
return nil
|
||||
default:
|
||||
panic("multiple rows updated")
|
||||
}
|
||||
}
|
||||
|
||||
func Peer_UpdateFull(
|
||||
tx TX,
|
||||
row *Peer,
|
||||
) (err error) {
|
||||
Peer_Sanitize(row)
|
||||
if err = Peer_Validate(row); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := tx.Exec("UPDATE peers SET APIKey=?,Name=?,IP=?,Port=?,Mediator=?,EncPubKey=?,SignPubKey=? WHERE PeerIP=?", row.APIKey, row.Name, row.IP, row.Port, row.Mediator, row.EncPubKey, row.SignPubKey, row.PeerIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
switch n {
|
||||
case 0:
|
||||
return sql.ErrNoRows
|
||||
case 1:
|
||||
return nil
|
||||
default:
|
||||
panic("multiple rows updated")
|
||||
}
|
||||
}
|
||||
|
||||
func Peer_Delete(
|
||||
tx TX,
|
||||
PeerIP byte,
|
||||
) (err error) {
|
||||
result, err := tx.Exec("DELETE FROM peers WHERE PeerIP=?", PeerIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
switch n {
|
||||
case 0:
|
||||
return sql.ErrNoRows
|
||||
case 1:
|
||||
return nil
|
||||
default:
|
||||
panic("multiple rows deleted")
|
||||
}
|
||||
}
|
||||
|
||||
func Peer_Get(
|
||||
tx TX,
|
||||
PeerIP byte,
|
||||
) (
|
||||
row *Peer,
|
||||
err error,
|
||||
) {
|
||||
row = &Peer{}
|
||||
r := tx.QueryRow("SELECT PeerIP,APIKey,Name,IP,Port,Mediator,EncPubKey,SignPubKey FROM peers WHERE PeerIP=?", PeerIP)
|
||||
err = r.Scan(&row.PeerIP, &row.APIKey, &row.Name, &row.IP, &row.Port, &row.Mediator, &row.EncPubKey, &row.SignPubKey)
|
||||
return
|
||||
}
|
||||
|
||||
func Peer_GetWhere(
|
||||
tx TX,
|
||||
query string,
|
||||
args ...any,
|
||||
) (
|
||||
row *Peer,
|
||||
err error,
|
||||
) {
|
||||
row = &Peer{}
|
||||
r := tx.QueryRow(query, args...)
|
||||
err = r.Scan(&row.PeerIP, &row.APIKey, &row.Name, &row.IP, &row.Port, &row.Mediator, &row.EncPubKey, &row.SignPubKey)
|
||||
return
|
||||
}
|
||||
|
||||
func Peer_Iterate(
|
||||
tx TX,
|
||||
query string,
|
||||
args ...any,
|
||||
) iter.Seq2[*Peer, error] {
|
||||
rows, err := tx.Query(query, args...)
|
||||
if err != nil {
|
||||
return func(yield func(*Peer, error) bool) {
|
||||
yield(nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
return func(yield func(*Peer, error) bool) {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
row := &Peer{}
|
||||
err := rows.Scan(&row.PeerIP, &row.APIKey, &row.Name, &row.IP, &row.Port, &row.Mediator, &row.EncPubKey, &row.SignPubKey)
|
||||
if !yield(row, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Peer_List(
|
||||
tx TX,
|
||||
query string,
|
||||
args ...any,
|
||||
) (
|
||||
l []*Peer,
|
||||
err error,
|
||||
) {
|
||||
for row, err := range Peer_Iterate(tx, query, args...) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l = append(l, row)
|
||||
}
|
||||
return l, nil
|
||||
}
|
69
hub/api/db/sanitize-validate.go
Normal file
69
hub/api/db/sanitize-validate.go
Normal file
@ -0,0 +1,69 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidIP = errors.New("invalid IP")
|
||||
ErrInvalidPort = errors.New("invalid port")
|
||||
)
|
||||
|
||||
func Config_Sanitize(c *Config) {
|
||||
if u, err := url.Parse(c.HubAddress); err == nil {
|
||||
c.HubAddress = u.String()
|
||||
}
|
||||
|
||||
if addr, ok := netip.AddrFromSlice(c.VPNNetwork); ok {
|
||||
c.VPNNetwork = addr.AsSlice()
|
||||
}
|
||||
}
|
||||
|
||||
func Config_Validate(c *Config) error {
|
||||
if _, err := url.Parse(c.HubAddress); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addr, ok := netip.AddrFromSlice(c.VPNNetwork)
|
||||
if !ok || !addr.Is4() || addr.As4()[3] != 0 || addr.As4()[0] == 0 {
|
||||
return ErrInvalidIP
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Session_Sanitize(s *Session) {
|
||||
}
|
||||
|
||||
func Session_Validate(s *Session) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Peer_Sanitize(p *Peer) {
|
||||
p.Name = strings.TrimSpace(p.Name)
|
||||
if len(p.IP) != 0 {
|
||||
addr, ok := netip.AddrFromSlice(p.IP)
|
||||
if ok && addr.Is4() {
|
||||
p.IP = addr.AsSlice()
|
||||
}
|
||||
}
|
||||
if p.Port == 0 {
|
||||
p.Port = 515
|
||||
}
|
||||
}
|
||||
|
||||
func Peer_Validate(p *Peer) error {
|
||||
if len(p.IP) > 0 {
|
||||
_, ok := netip.AddrFromSlice(p.IP)
|
||||
if !ok {
|
||||
return ErrInvalidIP
|
||||
}
|
||||
}
|
||||
if p.Port == 0 {
|
||||
return ErrInvalidPort
|
||||
}
|
||||
return nil
|
||||
}
|
25
hub/api/db/tables.defs
Normal file
25
hub/api/db/tables.defs
Normal file
@ -0,0 +1,25 @@
|
||||
TABLE config OF Config (
|
||||
ConfigID int64 PK,
|
||||
HubAddress string,
|
||||
VPNNetwork []byte,
|
||||
Password []byte NoUpdate
|
||||
);
|
||||
|
||||
TABLE sessions OF Session NoUpdate (
|
||||
SessionID string PK,
|
||||
CSRF string,
|
||||
SignedIn bool,
|
||||
CreatedAt int64,
|
||||
LastSeenAt int64
|
||||
);
|
||||
|
||||
TABLE peers OF Peer (
|
||||
PeerIP byte PK,
|
||||
APIKey string NoUpdate,
|
||||
Name string,
|
||||
IP []byte,
|
||||
Port uint16,
|
||||
Mediator bool,
|
||||
EncPubKey []byte NoUpdate,
|
||||
SignPubKey []byte NoUpdate
|
||||
);
|
51
hub/api/db/written.go
Normal file
51
hub/api/db/written.go
Normal file
@ -0,0 +1,51 @@
|
||||
package db
|
||||
|
||||
import "vppn/fasttime"
|
||||
|
||||
func Session_UpdateLastSeenAt(
|
||||
tx TX,
|
||||
id string,
|
||||
) (err error) {
|
||||
_, err = tx.Exec("UPDATE sessions SET LastSeenAt=? WHERE SessionID=?", fasttime.Now(), id)
|
||||
return err
|
||||
}
|
||||
|
||||
func Session_SetSignedIn(
|
||||
tx TX,
|
||||
id string,
|
||||
) (err error) {
|
||||
_, err = tx.Exec("UPDATE sessions SET SignedIn=1 WHERE SessionID=?", id)
|
||||
return err
|
||||
}
|
||||
|
||||
func Session_DeleteBefore(
|
||||
tx TX,
|
||||
timestamp int64,
|
||||
) (err error) {
|
||||
_, err = tx.Exec("DELETE FROM sessions WHERE LastSeenAt<?", timestamp)
|
||||
return err
|
||||
}
|
||||
|
||||
func Config_UpdatePassword(
|
||||
tx TX,
|
||||
pwdHash []byte,
|
||||
) (err error) {
|
||||
_, err = tx.Exec("UPDATE config SET Password=? WHERE ConfigID=1", pwdHash)
|
||||
return err
|
||||
}
|
||||
|
||||
func Peer_ListAll(tx TX) ([]*Peer, error) {
|
||||
return Peer_List(tx, Peer_SelectQuery)
|
||||
}
|
||||
|
||||
func Peer_GetByAPIKey(tx TX, apiKey string) (*Peer, error) {
|
||||
return Peer_GetWhere(
|
||||
tx,
|
||||
Peer_SelectQuery+` WHERE APIKey=?`,
|
||||
apiKey)
|
||||
}
|
||||
|
||||
func Peer_Exists(tx TX, ip byte) (exists bool, err error) {
|
||||
err = tx.QueryRow(`SELECT EXISTS(SELECT 1 FROM peers WHERE PeerIP=?)`, ip).Scan(&exists)
|
||||
return
|
||||
}
|
13
hub/api/errors.go
Normal file
13
hub/api/errors.go
Normal file
@ -0,0 +1,13 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"vppn/hub/api/db"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotAuthorized = errors.New("not authorized")
|
||||
ErrNoIPAvailable = errors.New("no IP address available")
|
||||
ErrInvalidIP = db.ErrInvalidIP
|
||||
ErrInvalidPort = db.ErrInvalidPort
|
||||
)
|
27
hub/api/migrations/2024-11-30-init.sql
Normal file
27
hub/api/migrations/2024-11-30-init.sql
Normal file
@ -0,0 +1,27 @@
|
||||
CREATE TABLE config (
|
||||
ConfigID INTEGER NOT NULL PRIMARY KEY, -- Always 1.
|
||||
HubAddress TEXT NOT NULL, -- https://for.example.com
|
||||
VPNNetwork BLOB NOT NULL, -- Network (/24), example 10.51.50.0
|
||||
Password BLOB NOT NULL -- bcrypt password for web interface
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE sessions (
|
||||
SessionID TEXT NOT NULL PRIMARY KEY,
|
||||
CSRF TEXT NOT NULL,
|
||||
SignedIn INTEGER NOT NULL,
|
||||
CreatedAt INTEGER NOT NULL,
|
||||
LastSeenAt INTEGER NOT NULL
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE INDEX sessions_last_seen_index ON sessions(LastSeenAt);
|
||||
|
||||
CREATE TABLE peers (
|
||||
PeerIP INTEGER NOT NULL PRIMARY KEY, -- Final byte.
|
||||
APIKey TEXT NOT NULL UNIQUE,
|
||||
Name TEXT NOT NULL UNIQUE, -- For humans.
|
||||
IP BLOB NOT NULL,
|
||||
Port INTEGER NOT NULL,
|
||||
Mediator INTEGER NOT NULL DEFAULT 0, -- Boolean if peer will forward packets. Must also have public address.
|
||||
EncPubKey BLOB NOT NULL,
|
||||
SignPubKey BLOB NOT NULL
|
||||
) WITHOUT ROWID;
|
1
hub/api/session.go
Normal file
1
hub/api/session.go
Normal file
@ -0,0 +1 @@
|
||||
package api
|
7
hub/api/time.go
Normal file
7
hub/api/time.go
Normal file
@ -0,0 +1,7 @@
|
||||
package api
|
||||
|
||||
import "time"
|
||||
|
||||
func timeSince(ts int64) int64 {
|
||||
return time.Now().Unix() - ts
|
||||
}
|
7
hub/api/types.go
Normal file
7
hub/api/types.go
Normal file
@ -0,0 +1,7 @@
|
||||
package api
|
||||
|
||||
import "vppn/hub/api/db"
|
||||
|
||||
type Config = db.Config
|
||||
type Session = db.Session
|
||||
type Peer = db.Peer
|
52
hub/app.go
Normal file
52
hub/app.go
Normal file
@ -0,0 +1,52 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"vppn/hub/api"
|
||||
|
||||
"git.crumpington.com/lib/webutil"
|
||||
)
|
||||
|
||||
//go:embed static
|
||||
var staticFS embed.FS
|
||||
|
||||
//go:embed templates
|
||||
var templateFS embed.FS
|
||||
|
||||
type Config struct {
|
||||
RootDir string
|
||||
ListenAddr string
|
||||
Secure bool
|
||||
}
|
||||
|
||||
type App struct {
|
||||
api *api.API
|
||||
mux *http.ServeMux
|
||||
tmpl map[string]*template.Template
|
||||
secure bool
|
||||
}
|
||||
|
||||
func NewApp(conf Config) (*App, error) {
|
||||
api, err := api.New(filepath.Join(conf.RootDir, "db.sqlite3"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
app := &App{
|
||||
api: api,
|
||||
mux: http.NewServeMux(),
|
||||
tmpl: webutil.ParseTemplateSet(templateFuncs, templateFS),
|
||||
secure: conf.Secure,
|
||||
}
|
||||
|
||||
app.registerRoutes()
|
||||
|
||||
return app, nil
|
||||
}
|
||||
|
||||
var templateFuncs = template.FuncMap{
|
||||
"ipToString": ipBytesTostring,
|
||||
}
|
33
hub/cookie.go
Normal file
33
hub/cookie.go
Normal file
@ -0,0 +1,33 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (a *App) getCookie(r *http.Request, name string) string {
|
||||
if c, err := r.Cookie(name); err == nil {
|
||||
return c.Value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *App) setCookie(w http.ResponseWriter, name, value string) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: name,
|
||||
Value: value,
|
||||
Path: "/",
|
||||
Secure: a.secure,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
MaxAge: 86400 * 365 * 10,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) deleteCookie(w http.ResponseWriter, name string) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: name,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
Expires: time.Unix(0, 0),
|
||||
})
|
||||
}
|
5
hub/global.go
Normal file
5
hub/global.go
Normal file
@ -0,0 +1,5 @@
|
||||
package hub
|
||||
|
||||
const (
|
||||
SESSION_ID_COOKIE_NAME = "SessionID"
|
||||
)
|
66
hub/handler.go
Normal file
66
hub/handler.go
Normal file
@ -0,0 +1,66 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"vppn/hub/api"
|
||||
|
||||
"git.crumpington.com/lib/webutil"
|
||||
)
|
||||
|
||||
type handlerFunc func(s *api.Session, w http.ResponseWriter, r *http.Request) error
|
||||
|
||||
func (app *App) handlePub(pattern string, fn handlerFunc) {
|
||||
wrapped := func(w http.ResponseWriter, r *http.Request) {
|
||||
sessionID := app.getCookie(r, SESSION_ID_COOKIE_NAME)
|
||||
s, err := app.api.Session_Get(sessionID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get session: %v", err)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if s.SessionID != sessionID {
|
||||
app.setCookie(w, SESSION_ID_COOKIE_NAME, s.SessionID)
|
||||
}
|
||||
|
||||
if r.Method == http.MethodPost {
|
||||
r.ParseMultipartForm(64 * 1024)
|
||||
if r.FormValue("CSRF") != s.CSRF {
|
||||
log.Printf("%s != %s", r.FormValue("CSRF"), s.CSRF)
|
||||
http.Error(w, "CSRF mismatch", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
r.ParseForm()
|
||||
}
|
||||
|
||||
if err := fn(s, w, r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
app.mux.HandleFunc(pattern,
|
||||
webutil.WithLogging(
|
||||
wrapped))
|
||||
}
|
||||
|
||||
func (app *App) handleNotSignedIn(pattern string, fn handlerFunc) {
|
||||
app.handlePub(pattern, func(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
if s.SignedIn {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return nil
|
||||
}
|
||||
return fn(s, w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (app *App) handleSignedIn(pattern string, fn handlerFunc) {
|
||||
app.handlePub(pattern, func(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
if !s.SignedIn {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return nil
|
||||
}
|
||||
return fn(s, w, r)
|
||||
})
|
||||
}
|
353
hub/handlers.go
Normal file
353
hub/handlers.go
Normal file
@ -0,0 +1,353 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"vppn/hub/api"
|
||||
"vppn/m"
|
||||
|
||||
"git.crumpington.com/lib/go/webutil"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func (a *App) _root(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
if s.SignedIn {
|
||||
return a.redirect(w, r, "/admin/config/")
|
||||
} else {
|
||||
return a.redirect(w, r, "/sign-in/")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) _signin(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
return a.render("/sign-in.html", w, struct{ Session *api.Session }{s})
|
||||
}
|
||||
|
||||
func (a *App) _signinSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
var pwd string
|
||||
err := webutil.NewFormScanner(r.Form).
|
||||
Scan("Password", &pwd).
|
||||
Error()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := a.api.Session_SignIn(s, pwd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.redirect(w, r, "/")
|
||||
}
|
||||
|
||||
func (a *App) _adminSignOut(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
return a.render("/admin-sign-out.html", w, struct{ Session *api.Session }{s})
|
||||
}
|
||||
|
||||
func (a *App) _adminSignOutSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
if err := a.api.Session_Delete(s.SessionID); err != nil {
|
||||
log.Printf("Failed to delete session cookie %s: %v", s.SessionID, err)
|
||||
}
|
||||
a.deleteCookie(w, SESSION_ID_COOKIE_NAME)
|
||||
return a.redirect(w, r, "/")
|
||||
}
|
||||
|
||||
func (a *App) _adminConfig(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
return a.render("/admin-config.html", w, struct {
|
||||
Session *api.Session
|
||||
Config *api.Config
|
||||
}{
|
||||
s,
|
||||
a.api.Config_Get(),
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) _adminConfigEdit(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
return a.render("/admin-config-edit.html", w, struct {
|
||||
Session *api.Session
|
||||
Config *api.Config
|
||||
}{
|
||||
s,
|
||||
a.api.Config_Get(),
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) _adminConfigEditSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
var (
|
||||
conf = a.api.Config_Get()
|
||||
ipStr string
|
||||
)
|
||||
|
||||
err := webutil.NewFormScanner(r.Form).
|
||||
Scan("HubAddress", &conf.HubAddress).
|
||||
Scan("VPNNetwork", &ipStr).
|
||||
Error()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if conf.VPNNetwork, err = stringToIP(ipStr); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.api.Config_Update(conf); err != nil {
|
||||
return err
|
||||
}
|
||||
return a.redirect(w, r, "/admin/config/")
|
||||
}
|
||||
|
||||
func (a *App) _adminPasswordEdit(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
return a.render("/admin-password-edit.html", w, struct{ Session *api.Session }{s})
|
||||
}
|
||||
|
||||
func (a *App) _adminPasswordSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
var (
|
||||
conf = a.api.Config_Get()
|
||||
curPwd string
|
||||
newPwd string
|
||||
newPwd2 string
|
||||
)
|
||||
|
||||
err := webutil.NewFormScanner(r.Form).
|
||||
Scan("CurrentPassword", &curPwd).
|
||||
Scan("NewPassword", &newPwd).
|
||||
Scan("NewPassword2", &newPwd2).
|
||||
Error()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(newPwd) < 8 {
|
||||
return errors.New("password is too short")
|
||||
}
|
||||
|
||||
if newPwd != newPwd2 {
|
||||
return errors.New("passwords don't match")
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword(conf.Password, []byte(curPwd))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(newPwd), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := a.api.Config_UpdatePassword(hash); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.redirect(w, r, "/admin/config/")
|
||||
}
|
||||
|
||||
func (a *App) _adminPeerList(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
peers, err := a.api.Peer_List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.render("/admin-peer-list.html", w, struct {
|
||||
Session *api.Session
|
||||
Peers []*api.Peer
|
||||
}{
|
||||
s,
|
||||
peers,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) _adminPeerCreate(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
return a.render("/admin-peer-create.html", w, struct{ Session *api.Session }{s})
|
||||
}
|
||||
|
||||
func (a *App) _adminPeerCreateSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
var ipStr string
|
||||
|
||||
args := api.PeerCreateArgs{}
|
||||
err := webutil.NewFormScanner(r.Form).
|
||||
Scan("Name", &args.Name).
|
||||
Scan("IP", &ipStr).
|
||||
Scan("Port", &args.Port).
|
||||
Scan("Mediator", &args.Mediator).
|
||||
Error()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if args.IP, err = stringToIP(ipStr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
code := a.api.Peer_CreateIntent(args)
|
||||
return a.redirect(w, r, "/admin/peer/intent-created/?Code=%s", code)
|
||||
}
|
||||
|
||||
func (a *App) _adminPeerIntentCreated(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
code := r.FormValue("Code")
|
||||
if code == "" {
|
||||
return errors.New("missing Code")
|
||||
}
|
||||
|
||||
return a.render("/admin-peer-intent.html", w, struct {
|
||||
Session *api.Session
|
||||
HubAddress string
|
||||
Code string
|
||||
}{s, a.api.Config_Get().HubAddress, code})
|
||||
}
|
||||
|
||||
func (a *App) _adminPeerView(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
var peerIP byte
|
||||
err := webutil.NewFormScanner(r.Form).Scan("PeerIP", &peerIP).Error()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peer, err := a.api.Peer_Get(peerIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.render("/admin-peer-view.html", w, struct {
|
||||
Session *api.Session
|
||||
Peer *api.Peer
|
||||
}{s, peer})
|
||||
}
|
||||
|
||||
func (a *App) _adminPeerEdit(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
var peerIP byte
|
||||
err := webutil.NewFormScanner(r.Form).Scan("PeerIP", &peerIP).Error()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peer, err := a.api.Peer_Get(peerIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.render("/admin-peer-edit.html", w, struct {
|
||||
Session *api.Session
|
||||
Peer *api.Peer
|
||||
}{s, peer})
|
||||
}
|
||||
|
||||
func (a *App) _adminPeerEditSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
var (
|
||||
peerIP byte
|
||||
ipStr string
|
||||
)
|
||||
|
||||
err := webutil.NewFormScanner(r.Form).Scan("PeerIP", &peerIP).Error()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peer, err := a.api.Peer_Get(peerIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = webutil.NewFormScanner(r.Form).
|
||||
Scan("Name", &peer.Name).
|
||||
Scan("IP", &ipStr).
|
||||
Scan("Port", &peer.Port).
|
||||
Scan("Mediator", &peer.Mediator).
|
||||
Error()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if peer.IP, err = stringToIP(ipStr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = a.api.Peer_Update(peer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.redirect(w, r, "/admin/peer/view/?PeerIP=%d", peer.PeerIP)
|
||||
}
|
||||
|
||||
func (a *App) _adminPeerDelete(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
var peerIP byte
|
||||
err := webutil.NewFormScanner(r.Form).Scan("PeerIP", &peerIP).Error()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peer, err := a.api.Peer_Get(peerIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.render("/admin-peer-delete.html", w, struct {
|
||||
Session *api.Session
|
||||
Peer *api.Peer
|
||||
}{s, peer})
|
||||
}
|
||||
|
||||
func (a *App) _adminPeerDeleteSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
var peerIP byte
|
||||
err := webutil.NewFormScanner(r.Form).Scan("PeerIP", &peerIP).Error()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := a.api.Peer_Delete(peerIP); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.redirect(w, r, "/admin/peer/list/")
|
||||
}
|
||||
|
||||
func (a *App) _peerCreate(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
code := r.FormValue("Code")
|
||||
conf, err := a.api.Peer_Create(code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.sendJSON(w, conf)
|
||||
}
|
||||
|
||||
func (a *App) _peerFetchState(s *api.Session, w http.ResponseWriter, r *http.Request) error {
|
||||
_, apiKey, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
log.Printf("1")
|
||||
return api.ErrNotAuthorized
|
||||
}
|
||||
|
||||
peer, err := a.api.Peer_GetByAPIKey(apiKey)
|
||||
if err != nil {
|
||||
log.Printf("2")
|
||||
return err
|
||||
}
|
||||
|
||||
peers, err := a.api.Peer_List()
|
||||
if err != nil {
|
||||
log.Printf("3")
|
||||
return err
|
||||
}
|
||||
|
||||
conf := a.api.Config_Get()
|
||||
|
||||
state := m.NetworkState{
|
||||
HubAddress: conf.HubAddress,
|
||||
Network: conf.VPNNetwork,
|
||||
PeerIP: peer.PeerIP,
|
||||
IP: peer.IP,
|
||||
Port: peer.Port,
|
||||
}
|
||||
|
||||
for _, p := range peers {
|
||||
state.Peers[p.PeerIP] = &m.Peer{
|
||||
PeerIP: p.PeerIP,
|
||||
Name: p.Name,
|
||||
IP: p.IP,
|
||||
Port: p.Port,
|
||||
Mediator: p.Mediator,
|
||||
EncPubKey: p.EncPubKey,
|
||||
SignPubKey: p.SignPubKey,
|
||||
}
|
||||
}
|
||||
|
||||
return a.sendJSON(w, state)
|
||||
}
|
38
hub/main.go
Normal file
38
hub/main.go
Normal file
@ -0,0 +1,38 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"git.crumpington.com/lib/webutil"
|
||||
)
|
||||
|
||||
func Main() {
|
||||
log.SetFlags(0)
|
||||
|
||||
conf := Config{}
|
||||
flag.StringVar(&conf.RootDir, "root-dir", "", "[REQUIRED] Root directory.")
|
||||
flag.StringVar(&conf.ListenAddr, "listen", "", "[REQUIRED] Listen address.")
|
||||
flag.BoolVar(&conf.Secure, "secure", false, "Use secure cookies.")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if conf.RootDir == "" || conf.ListenAddr == "" {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
app, err := NewApp(conf)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: conf.ListenAddr,
|
||||
Handler: app.mux,
|
||||
}
|
||||
|
||||
log.Fatal(webutil.ListenAndServe(srv))
|
||||
}
|
31
hub/routes.go
Normal file
31
hub/routes.go
Normal file
@ -0,0 +1,31 @@
|
||||
package hub
|
||||
|
||||
import "net/http"
|
||||
|
||||
func (a *App) registerRoutes() {
|
||||
a.mux.Handle("GET /static/", http.FileServerFS(staticFS))
|
||||
a.handlePub("GET /", a._root)
|
||||
|
||||
a.handleNotSignedIn("GET /sign-in/", a._signin)
|
||||
a.handleNotSignedIn("POST /sign-in/", a._signinSubmit)
|
||||
|
||||
a.handleSignedIn("GET /admin/config/", a._adminConfig)
|
||||
a.handleSignedIn("GET /admin/config/edit/", a._adminConfigEdit)
|
||||
a.handleSignedIn("POST /admin/config/edit/", a._adminConfigEditSubmit)
|
||||
a.handleSignedIn("GET /admin/sign-out/", a._adminSignOut)
|
||||
a.handleSignedIn("POST /admin/sign-out/", a._adminSignOutSubmit)
|
||||
a.handleSignedIn("GET /admin/password/edit/", a._adminPasswordEdit)
|
||||
a.handleSignedIn("POST /admin/password/edit/", a._adminPasswordSubmit)
|
||||
a.handleSignedIn("GET /admin/peer/list/", a._adminPeerList)
|
||||
a.handleSignedIn("GET /admin/peer/create/", a._adminPeerCreate)
|
||||
a.handleSignedIn("POST /admin/peer/create/", a._adminPeerCreateSubmit)
|
||||
a.handleSignedIn("GET /admin/peer/intent-created/", a._adminPeerIntentCreated)
|
||||
a.handleSignedIn("GET /admin/peer/view/", a._adminPeerView)
|
||||
a.handleSignedIn("GET /admin/peer/edit/", a._adminPeerEdit)
|
||||
a.handleSignedIn("POST /admin/peer/edit/", a._adminPeerEditSubmit)
|
||||
a.handleSignedIn("GET /admin/peer/delete/", a._adminPeerDelete)
|
||||
a.handleSignedIn("POST /admin/peer/delete/", a._adminPeerDeleteSubmit)
|
||||
|
||||
a.handleNotSignedIn("GET /peer/create/", a._peerCreate)
|
||||
a.handleNotSignedIn("GET /peer/fetch-state/", a._peerFetchState)
|
||||
}
|
22
hub/static/custom.css
Normal file
22
hub/static/custom.css
Normal file
@ -0,0 +1,22 @@
|
||||
body {
|
||||
max-width: 1920px;
|
||||
}
|
||||
|
||||
.def-list tr td:first-child {
|
||||
text-align:right;
|
||||
width:1%;
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
width:100%;
|
||||
max-width:640px;
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
width:unset;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
8
hub/static/new.min.css
vendored
Normal file
8
hub/static/new.min.css
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Minified by jsDelivr using clean-css v4.2.1.
|
||||
* Original file: /npm/@exampledev/new.css@1.1.3/new.css
|
||||
*
|
||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||
*/
|
||||
:root{--nc-font-sans:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";--nc-font-mono:Consolas,monaco,'Ubuntu Mono','Liberation Mono','Courier New',Courier,monospace;--nc-tx-1:#000000;--nc-tx-2:#1A1A1A;--nc-bg-1:#FFFFFF;--nc-bg-2:#F6F8FA;--nc-bg-3:#E5E7EB;--nc-lk-1:#0070F3;--nc-lk-2:#0366D6;--nc-lk-tx:#FFFFFF;--nc-ac-1:#79FFE1;--nc-ac-tx:#0C4047}@media (prefers-color-scheme:dark){:root{--nc-tx-1:#ffffff;--nc-tx-2:#eeeeee;--nc-bg-1:#000000;--nc-bg-2:#111111;--nc-bg-3:#222222;--nc-lk-1:#3291FF;--nc-lk-2:#0070F3;--nc-lk-tx:#FFFFFF;--nc-ac-1:#7928CA;--nc-ac-tx:#FFFFFF}}*{margin:0;padding:0}address,area,article,aside,audio,blockquote,datalist,details,dl,fieldset,figure,form,iframe,img,input,meter,nav,ol,optgroup,option,output,p,pre,progress,ruby,section,table,textarea,ul,video{margin-bottom:1rem}button,html,input,select{font-family:var(--nc-font-sans)}body{margin:0 auto;max-width:750px;padding:2rem;border-radius:6px;overflow-x:hidden;word-break:break-word;overflow-wrap:break-word;background:var(--nc-bg-1);color:var(--nc-tx-2);font-size:1.03rem;line-height:1.5}::selection{background:var(--nc-ac-1);color:var(--nc-ac-tx)}h1,h2,h3,h4,h5,h6{line-height:1;color:var(--nc-tx-1);padding-top:.875rem}h1,h2,h3{color:var(--nc-tx-1);padding-bottom:2px;margin-bottom:8px;border-bottom:1px solid var(--nc-bg-2)}h4,h5,h6{margin-bottom:.3rem}h1{font-size:2.25rem}h2{font-size:1.85rem}h3{font-size:1.55rem}h4{font-size:1.25rem}h5{font-size:1rem}h6{font-size:.875rem}a{color:var(--nc-lk-1)}a:hover{color:var(--nc-lk-2)}abbr:hover{cursor:help}blockquote{padding:1.5rem;background:var(--nc-bg-2);border-left:5px solid var(--nc-bg-3)}abbr{cursor:help}blockquote :last-child{padding-bottom:0;margin-bottom:0}header{background:var(--nc-bg-2);border-bottom:1px solid var(--nc-bg-3);padding:2rem 1.5rem;margin:-2rem calc(0px - (50vw - 50%)) 2rem;padding-left:calc(50vw - 50%);padding-right:calc(50vw - 50%)}header h1,header h2,header h3{padding-bottom:0;border-bottom:0}header>:first-child{margin-top:0;padding-top:0}header>:last-child{margin-bottom:0}a button,button,input[type=button],input[type=reset],input[type=submit]{font-size:1rem;display:inline-block;padding:6px 12px;text-align:center;text-decoration:none;white-space:nowrap;background:var(--nc-lk-1);color:var(--nc-lk-tx);border:0;border-radius:4px;box-sizing:border-box;cursor:pointer;color:var(--nc-lk-tx)}a button[disabled],button[disabled],input[type=button][disabled],input[type=reset][disabled],input[type=submit][disabled]{cursor:default;opacity:.5;cursor:not-allowed}.button:focus,.button:hover,button:focus,button:hover,input[type=button]:focus,input[type=button]:hover,input[type=reset]:focus,input[type=reset]:hover,input[type=submit]:focus,input[type=submit]:hover{background:var(--nc-lk-2)}code,kbd,pre,samp{font-family:var(--nc-font-mono)}code,kbd,pre,samp{background:var(--nc-bg-2);border:1px solid var(--nc-bg-3);border-radius:4px;padding:3px 6px;font-size:.9rem}kbd{border-bottom:3px solid var(--nc-bg-3)}pre{padding:1rem 1.4rem;max-width:100%;overflow:auto}pre code{background:inherit;font-size:inherit;color:inherit;border:0;padding:0;margin:0}code pre{display:inline;background:inherit;font-size:inherit;color:inherit;border:0;padding:0;margin:0}details{padding:.6rem 1rem;background:var(--nc-bg-2);border:1px solid var(--nc-bg-3);border-radius:4px}summary{cursor:pointer;font-weight:700}details[open]{padding-bottom:.75rem}details[open] summary{margin-bottom:6px}details[open]>:last-child{margin-bottom:0}dt{font-weight:700}dd::before{content:'→ '}hr{border:0;border-bottom:1px solid var(--nc-bg-3);margin:1rem auto}fieldset{margin-top:1rem;padding:2rem;border:1px solid var(--nc-bg-3);border-radius:4px}legend{padding:auto .5rem}table{border-collapse:collapse;width:100%}td,th{border:1px solid var(--nc-bg-3);text-align:left;padding:.5rem}th{background:var(--nc-bg-2)}tr:nth-child(even){background:var(--nc-bg-2)}table caption{font-weight:700;margin-bottom:.5rem}textarea{max-width:100%}ol,ul{padding-left:2rem}li{margin-top:.4rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}mark{padding:3px 6px;background:var(--nc-ac-1);color:var(--nc-ac-tx)}input,select,textarea{padding:6px 12px;margin-bottom:.5rem;background:var(--nc-bg-2);color:var(--nc-tx-2);border:1px solid var(--nc-bg-3);border-radius:4px;box-shadow:none;box-sizing:border-box}img{max-width:100%}
|
||||
/*# sourceMappingURL=/sm/4a51164882967d28a74fabce02685c18fa45a529b77514edc75d708f04dd08b9.map */
|
20
hub/templates/admin-config-edit.html
Normal file
20
hub/templates/admin-config-edit.html
Normal file
@ -0,0 +1,20 @@
|
||||
{{define "body" -}}
|
||||
<h2>Config</h2>
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="CSRF" value="{{.Session.CSRF}}">
|
||||
<p>
|
||||
<label>Hub Address</label><br>
|
||||
<input type="url" name="HubAddress" value="{{.Config.HubAddress}}">
|
||||
</p>
|
||||
<p>
|
||||
<label>VPN Network</label><br>
|
||||
<input type="text" name="VPNNetwork" value="{{ipToString .Config.VPNNetwork}}">
|
||||
</p>
|
||||
<p>
|
||||
<button type="submit">Save</button>
|
||||
<a href="/admin/config/">Cancel</a>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
{{- end}}
|
19
hub/templates/admin-config.html
Normal file
19
hub/templates/admin-config.html
Normal file
@ -0,0 +1,19 @@
|
||||
{{define "body" -}}
|
||||
<h2>Config</h2>
|
||||
|
||||
<p>
|
||||
<a href="/admin/config/edit/">Edit</a> /
|
||||
<a href="/admin/password/edit/">Change Password</a>
|
||||
</p>
|
||||
|
||||
<table class="def-list">
|
||||
<tr>
|
||||
<td>Hub Address</td>
|
||||
<td>{{.Config.HubAddress}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VPN Network</td>
|
||||
<td>{{ipToString .Config.VPNNetwork}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{- end}}
|
23
hub/templates/admin-password-edit.html
Normal file
23
hub/templates/admin-password-edit.html
Normal file
@ -0,0 +1,23 @@
|
||||
{{define "body" -}}
|
||||
<h2>Change Password</h2>
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="CSRF" value="{{.Session.CSRF}}">
|
||||
<p>
|
||||
<label>Current Password</label><br>
|
||||
<input type="password" name="CurrentPassword">
|
||||
</p>
|
||||
<p>
|
||||
<label>New Password</label><br>
|
||||
<input type="password" name="NewPassword">
|
||||
</p>
|
||||
<p>
|
||||
<label>Repeat New Password</label><br>
|
||||
<input type="password" name="NewPassword2">
|
||||
</p>
|
||||
<p>
|
||||
<button type="submit">Save</button>
|
||||
<a href="/admin/config/">Cancel</a>
|
||||
</p>
|
||||
</form>
|
||||
{{- end}}
|
30
hub/templates/admin-peer-create.html
Normal file
30
hub/templates/admin-peer-create.html
Normal file
@ -0,0 +1,30 @@
|
||||
{{define "body" -}}
|
||||
<h2>New Peer</h2>
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="CSRF" value="{{.Session.CSRF}}">
|
||||
<p>
|
||||
<label>Name</label><br>
|
||||
<input type="text" name="Name">
|
||||
</p>
|
||||
<p>
|
||||
<label>IP</label><br>
|
||||
<input type="text" name="IP">
|
||||
</p>
|
||||
<p>
|
||||
<label>Port</label><br>
|
||||
<input type="number" name="Port" value="515">
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" name="Mediator">
|
||||
Mediator
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<button type="submit">Save</button>
|
||||
<a href="/admin/config/">Cancel</a>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
{{- end}}
|
36
hub/templates/admin-peer-delete.html
Normal file
36
hub/templates/admin-peer-delete.html
Normal file
@ -0,0 +1,36 @@
|
||||
{{define "body" -}}
|
||||
<h2>Delete Peer</h2>
|
||||
|
||||
{{with .Peer -}}
|
||||
<form method="POST">
|
||||
<input type="hidden" name="CSRF" value="{{$.Session.CSRF}}">
|
||||
<p>
|
||||
<label>Peer IP</label><br>
|
||||
<input type="number" name="PeerIP" value="{{.PeerIP}}" disabled>
|
||||
</p>
|
||||
<p>
|
||||
<label>Name</label><br>
|
||||
<input type="text" value="{{.Name}}" disabled>
|
||||
</p>
|
||||
<p>
|
||||
<label>IP</label><br>
|
||||
<input type="text" value="{{ipToString .IP}}" disabled>
|
||||
</p>
|
||||
<p>
|
||||
<label>Port</label><br>
|
||||
<input type="number" value="{{.Port}}" disabled>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" {{if .Mediator}}checked{{end}} disabled>
|
||||
Mediator
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<button type="submit">Delete</button>
|
||||
<a href="/admin/peer/view/?PeerIP={{.PeerIP}}">Cancel</a>
|
||||
</p>
|
||||
</form>
|
||||
{{- end}}
|
||||
|
||||
{{- end}}
|
36
hub/templates/admin-peer-edit.html
Normal file
36
hub/templates/admin-peer-edit.html
Normal file
@ -0,0 +1,36 @@
|
||||
{{define "body" -}}
|
||||
<h2>Edit Peer</h2>
|
||||
|
||||
{{with .Peer -}}
|
||||
<form method="POST">
|
||||
<input type="hidden" name="CSRF" value="{{$.Session.CSRF}}">
|
||||
<p>
|
||||
<label>Peer IP</label><br>
|
||||
<input type="number" name="PeerIP" value="{{.PeerIP}}" disabled>
|
||||
</p>
|
||||
<p>
|
||||
<label>Name</label><br>
|
||||
<input type="text" name="Name" value="{{.Name}}">
|
||||
</p>
|
||||
<p>
|
||||
<label>IP</label><br>
|
||||
<input type="text" name="IP" value="{{ipToString .IP}}">
|
||||
</p>
|
||||
<p>
|
||||
<label>Port</label><br>
|
||||
<input type="number" name="Port" value="{{.Port}}">
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" name="Mediator" {{if .Mediator}}checked{{end}}>
|
||||
Mediator
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<button type="submit">Save</button>
|
||||
<a href="/admin/peer/view/?PeerIP={{.PeerIP}}">Cancel</a>
|
||||
</p>
|
||||
</form>
|
||||
{{- end}}
|
||||
|
||||
{{- end}}
|
13
hub/templates/admin-peer-intent.html
Normal file
13
hub/templates/admin-peer-intent.html
Normal file
@ -0,0 +1,13 @@
|
||||
{{define "body" -}}
|
||||
<h2>Create Peer</h2>
|
||||
|
||||
<p>
|
||||
Configure the peer with the following URL:
|
||||
</p>
|
||||
<pre>
|
||||
{{.HubAddress}}/peer/create/?Code={{.Code}}
|
||||
</pre>
|
||||
<p>
|
||||
<a href="/admin/peer/list/">Done</a>
|
||||
</p>
|
||||
{{- end}}
|
39
hub/templates/admin-peer-list.html
Normal file
39
hub/templates/admin-peer-list.html
Normal file
@ -0,0 +1,39 @@
|
||||
{{define "body" -}}
|
||||
<h2>Peers</h2>
|
||||
|
||||
<p>
|
||||
<a href="/admin/peer/create/">Add Peer</a>
|
||||
</p>
|
||||
|
||||
{{if .Peers -}}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>PeerIP</th>
|
||||
<th>Name</th>
|
||||
<th>IP</th>
|
||||
<th>Port</th>
|
||||
<th>Mediator</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Peers -}}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/admin/peer/view/?PeerIP={{.PeerIP}}">
|
||||
{{.PeerIP}}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{.Name}}</td>
|
||||
<td>{{ipToString .IP}}</td>
|
||||
<td>{{.Port}}</td>
|
||||
<td>{{if .Mediator}}T{{else}}F{{end}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{{- end}}
|
||||
</table>
|
||||
{{- else}}
|
||||
<p>No peers.</p>
|
||||
{{- end}}
|
||||
|
||||
{{- end}}
|
20
hub/templates/admin-peer-view.html
Normal file
20
hub/templates/admin-peer-view.html
Normal file
@ -0,0 +1,20 @@
|
||||
{{define "body" -}}
|
||||
<h2>Peer</h2>
|
||||
|
||||
<p>
|
||||
<a href="/admin/peer/edit/?PeerIP={{.Peer.PeerIP}}">Edit</a> /
|
||||
<a href="/admin/peer/delete/?PeerIP={{.Peer.PeerIP}}">Delete</a>
|
||||
</p>
|
||||
|
||||
{{with .Peer -}}
|
||||
<table class="def-list">
|
||||
<tr><td>Peer IP</td><td>{{.PeerIP}}</td></tr>
|
||||
<tr><td>Name</td><td>{{.Name}}</td></tr>
|
||||
<tr><td>IP</td><td>{{ipToString .IP}}</td></tr>
|
||||
<tr><td>Port</td><td>{{.Port}}</td></tr>
|
||||
<tr><td>Mediator</td><td>{{if .Mediator}}T{{else}}F{{end}}</td></tr>
|
||||
<tr><td>API Key</td><td>{{.APIKey}}</td></tr>
|
||||
</table>
|
||||
{{- end}}
|
||||
|
||||
{{- end}}
|
11
hub/templates/admin-sign-out.html
Normal file
11
hub/templates/admin-sign-out.html
Normal file
@ -0,0 +1,11 @@
|
||||
{{define "body" -}}
|
||||
<h2>Sign Out</h2>
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="CSRF" value="{{.Session.CSRF}}">
|
||||
<p>
|
||||
<button type="submit">Sign Out</button>
|
||||
<a href="/admin/config/">Cancel</a>
|
||||
</p>
|
||||
</form>
|
||||
{{- end}}
|
21
hub/templates/base.html
Normal file
21
hub/templates/base.html
Normal file
@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>VPPN Hub</title>
|
||||
<link rel="stylesheet" href="/static/new.min.css">
|
||||
<link rel="stylesheet" href="/static/custom.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>VPPN</h1>
|
||||
<nav>
|
||||
{{if .Session.SignedIn -}}
|
||||
<a href="/admin/config/">Config</a> /
|
||||
<a href="/admin/peer/list/">Peers</a> /
|
||||
<a href="/admin/sign-out/">Sign out</a>
|
||||
{{- end}}
|
||||
</nav>
|
||||
</header>
|
||||
{{block "body" .}}There's nothing here.{{end}}
|
||||
</body>
|
||||
</html>
|
0
hub/templates/share/common.html
Normal file
0
hub/templates/share/common.html
Normal file
14
hub/templates/sign-in.html
Normal file
14
hub/templates/sign-in.html
Normal file
@ -0,0 +1,14 @@
|
||||
{{define "body" -}}
|
||||
<h2>Sign In</h2>
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="CSRF" value="{{.Session.CSRF}}">
|
||||
<p>
|
||||
<label>Password</label><br>
|
||||
<input type="password" name="Password">
|
||||
</p>
|
||||
<p>
|
||||
<button type="submit">Submit</button>
|
||||
</p>
|
||||
</form>
|
||||
{{- end}}
|
1
hub/time.go
Normal file
1
hub/time.go
Normal file
@ -0,0 +1 @@
|
||||
package hub
|
59
hub/util.go
Normal file
59
hub/util.go
Normal file
@ -0,0 +1,59 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (app *App) render(name string, w http.ResponseWriter, data any) error {
|
||||
tmpl, ok := app.tmpl[name]
|
||||
if !ok {
|
||||
log.Printf("Template not found: %s", name)
|
||||
return errors.New("not found")
|
||||
}
|
||||
if err := tmpl.Execute(w, data); err != nil {
|
||||
log.Printf("Failed to render template %s: %v", name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *App) redirect(w http.ResponseWriter, r *http.Request, url string, args ...any) error {
|
||||
if len(args) > 0 {
|
||||
url = fmt.Sprintf(url, args...)
|
||||
}
|
||||
http.Redirect(w, r, url, http.StatusSeeOther)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *App) sendJSON(w http.ResponseWriter, data any) error {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(data); err != nil {
|
||||
log.Printf("Failed to send JSON: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stringToIP(in string) ([]byte, error) {
|
||||
in = strings.TrimSpace(in)
|
||||
if len(in) == 0 {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
addr, err := netip.ParseAddr(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return addr.AsSlice(), nil
|
||||
}
|
||||
|
||||
func ipBytesTostring(in []byte) string {
|
||||
if addr, ok := netip.AddrFromSlice(in); ok {
|
||||
return addr.String()
|
||||
}
|
||||
return ""
|
||||
}
|
39
m/models.go
Normal file
39
m/models.go
Normal file
@ -0,0 +1,39 @@
|
||||
// The package `m` contains models shared between the hub and peer programs.
|
||||
package m
|
||||
|
||||
type PeerConfig struct {
|
||||
PeerIP byte
|
||||
HubAddress string
|
||||
Network []byte
|
||||
APIKey string
|
||||
IP []byte
|
||||
Port uint16
|
||||
Mediator bool
|
||||
EncPubKey []byte
|
||||
EncPrivKey []byte
|
||||
SignPubKey []byte
|
||||
SignPrivKey []byte
|
||||
}
|
||||
|
||||
type Peer struct {
|
||||
PeerIP byte
|
||||
Name string
|
||||
IP []byte
|
||||
Port uint16
|
||||
Mediator bool
|
||||
EncPubKey []byte
|
||||
SignPubKey []byte
|
||||
}
|
||||
|
||||
type NetworkState struct {
|
||||
HubAddress string
|
||||
|
||||
// The requester's data:
|
||||
Network []byte
|
||||
PeerIP byte
|
||||
IP []byte
|
||||
Port uint16
|
||||
|
||||
// All peer data.
|
||||
Peers [256]*Peer
|
||||
}
|
32
peer/crypto.go
Normal file
32
peer/crypto.go
Normal file
@ -0,0 +1,32 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/crypto/nacl/sign"
|
||||
)
|
||||
|
||||
func encryptPacket(sharedKey, nonce, packet, out []byte) []byte {
|
||||
out = box.SealAfterPrecomputation(out[:0], packet, (*[24]byte)(nonce), (*[32]byte)(sharedKey))
|
||||
return append(out, nonce...)
|
||||
}
|
||||
|
||||
func decryptPacket(sharedKey, packet, out []byte) (decrypted []byte, ok bool) {
|
||||
cut := len(packet) - NONCE_SIZE
|
||||
decrypted, ok = box.OpenAfterPrecomputation(out[:0], packet[:cut], (*[24]byte)(packet[cut:]), (*[32]byte)(sharedKey))
|
||||
return decrypted, ok
|
||||
}
|
||||
|
||||
// Signed packet should be encrypted with the encryptPacket function first.
|
||||
func signPacket(privKey, packet, out []byte) []byte {
|
||||
return sign.Sign(out[:0], packet, (*[64]byte)(privKey))
|
||||
}
|
||||
|
||||
func openPacket(pubKey, packet, out []byte) (encPacket []byte, ok bool) {
|
||||
return sign.Open(out[:0], packet, (*[32]byte)(pubKey))
|
||||
}
|
||||
|
||||
func computeSharedKey(peerPubKey, privKey []byte) []byte {
|
||||
shared := [32]byte{}
|
||||
box.Precompute(&shared, (*[32]byte)(peerPubKey), (*[32]byte)(privKey))
|
||||
return shared[:]
|
||||
}
|
129
peer/crypto_test.go
Normal file
129
peer/crypto_test.go
Normal file
@ -0,0 +1,129 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/crypto/nacl/sign"
|
||||
)
|
||||
|
||||
func TestEncryptDecryptPacket(t *testing.T) {
|
||||
pubKey1, privKey1, err := box.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pubKey2, privKey2, err := box.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sharedEncKey := [32]byte{}
|
||||
box.Precompute(&sharedEncKey, pubKey2, privKey1)
|
||||
|
||||
sharedDecKey := [32]byte{}
|
||||
box.Precompute(&sharedDecKey, pubKey1, privKey2)
|
||||
|
||||
original := make([]byte, MTU)
|
||||
rand.Read(original)
|
||||
|
||||
nonce := make([]byte, NONCE_SIZE)
|
||||
rand.Read(nonce)
|
||||
|
||||
encrypted := make([]byte, BUFFER_SIZE)
|
||||
encrypted = encryptPacket(sharedEncKey[:], nonce, original, encrypted)
|
||||
|
||||
decrypted := make([]byte, MTU)
|
||||
var ok bool
|
||||
decrypted, ok = decryptPacket(sharedDecKey[:], encrypted, decrypted)
|
||||
if !ok {
|
||||
t.Fatal(ok)
|
||||
}
|
||||
|
||||
if !bytes.Equal(original, decrypted) {
|
||||
t.Fatal("mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEncryptPacket(b *testing.B) {
|
||||
_, privKey1, err := box.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
pubKey2, _, err := box.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
sharedEncKey := [32]byte{}
|
||||
box.Precompute(&sharedEncKey, pubKey2, privKey1)
|
||||
|
||||
original := make([]byte, MTU)
|
||||
rand.Read(original)
|
||||
|
||||
nonce := make([]byte, NONCE_SIZE)
|
||||
rand.Read(nonce)
|
||||
|
||||
encrypted := make([]byte, BUFFER_SIZE)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
encrypted = encryptPacket(sharedEncKey[:], nonce, original, encrypted)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDecryptPacket(b *testing.B) {
|
||||
pubKey1, privKey1, err := box.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
pubKey2, privKey2, err := box.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
sharedEncKey := [32]byte{}
|
||||
box.Precompute(&sharedEncKey, pubKey2, privKey1)
|
||||
|
||||
sharedDecKey := [32]byte{}
|
||||
box.Precompute(&sharedDecKey, pubKey1, privKey2)
|
||||
|
||||
original := make([]byte, MTU)
|
||||
rand.Read(original)
|
||||
|
||||
nonce := make([]byte, NONCE_SIZE)
|
||||
rand.Read(nonce)
|
||||
|
||||
encrypted := make([]byte, BUFFER_SIZE)
|
||||
encrypted = encryptPacket(sharedEncKey[:], nonce, original, encrypted)
|
||||
|
||||
decrypted := make([]byte, MTU)
|
||||
for i := 0; i < b.N; i++ {
|
||||
decrypted, _ = decryptPacket(sharedDecKey[:], encrypted, decrypted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignOpenPacket(t *testing.T) {
|
||||
pubKey, privKey, err := sign.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
packet := make([]byte, MTU)
|
||||
|
||||
rand.Read(packet)
|
||||
|
||||
signedPacket := signPacket(privKey[:], packet, make([]byte, BUFFER_SIZE))
|
||||
|
||||
encPacket, ok := openPacket(pubKey[:], signedPacket, make([]byte, BUFFER_SIZE))
|
||||
if !ok {
|
||||
t.Fatal(ok)
|
||||
}
|
||||
|
||||
if !bytes.Equal(encPacket, packet) {
|
||||
t.Fatal("not equal")
|
||||
}
|
||||
}
|
82
peer/files.go
Normal file
82
peer/files.go
Normal file
@ -0,0 +1,82 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"vppn/m"
|
||||
)
|
||||
|
||||
func configDir(netName string) string {
|
||||
d, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get user home directory: %v", err)
|
||||
}
|
||||
return filepath.Join(d, ".vppn", netName)
|
||||
}
|
||||
|
||||
func peerConfigPath(netName string) string {
|
||||
return filepath.Join(configDir(netName), "peer-config.json")
|
||||
}
|
||||
|
||||
func peerStatePath(netName string) string {
|
||||
return filepath.Join(configDir(netName), "peer-state.json")
|
||||
}
|
||||
|
||||
func storeJson(x any, outPath string) error {
|
||||
outDir := filepath.Dir(outPath)
|
||||
_ = os.MkdirAll(outDir, 0700)
|
||||
|
||||
tmpPath := outPath + ".tmp"
|
||||
buf, err := json.Marshal(x)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.Create(tmpPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := f.Write(buf); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f.Sync(); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Rename(tmpPath, outPath)
|
||||
}
|
||||
|
||||
func storePeerConfig(netName string, pc m.PeerConfig) error {
|
||||
return storeJson(pc, peerConfigPath(netName))
|
||||
}
|
||||
|
||||
func storePeerState(netName string, ps m.NetworkState) error {
|
||||
return storeJson(ps, peerStatePath(netName))
|
||||
}
|
||||
|
||||
func loadJson(dataPath string, ptr any) error {
|
||||
data, err := os.ReadFile(dataPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, ptr)
|
||||
}
|
||||
|
||||
func loadPeerConfig(netName string) (pc m.PeerConfig, err error) {
|
||||
return pc, loadJson(peerConfigPath(netName), &pc)
|
||||
}
|
||||
|
||||
func loadPeerState(netName string) (ps m.NetworkState, err error) {
|
||||
return ps, loadJson(peerStatePath(netName), &ps)
|
||||
}
|
16
peer/globals.go
Normal file
16
peer/globals.go
Normal file
@ -0,0 +1,16 @@
|
||||
package peer
|
||||
|
||||
const (
|
||||
DEFAULT_PORT = 515
|
||||
NONCE_SIZE = 24
|
||||
KEY_SIZE = 32
|
||||
SIG_SIZE = 64
|
||||
MTU = 1408
|
||||
BUFFER_SIZE = MTU + NONCE_SIZE + SIG_SIZE
|
||||
|
||||
STREAM_DATA = 0
|
||||
STREAM_ROUTING = 1 // Routing queries and responses.
|
||||
|
||||
// Basic packet types
|
||||
PACKET_TYPE_DATA = 0
|
||||
)
|
86
peer/main.go
Normal file
86
peer/main.go
Normal file
@ -0,0 +1,86 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"vppn/m"
|
||||
|
||||
_ "net/http/pprof"
|
||||
)
|
||||
|
||||
func Main() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("stacktrace from panic: \n" + string(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
log.Printf("Serving on localhost:6060...")
|
||||
log.Println(http.ListenAndServe("localhost:6060", nil))
|
||||
}()
|
||||
|
||||
var (
|
||||
netName string
|
||||
initURL string
|
||||
listenIP string
|
||||
port int
|
||||
)
|
||||
|
||||
flag.StringVar(&netName, "name", "", "[REQUIRED] The network name.")
|
||||
flag.StringVar(&initURL, "init-url", "", "Initializes peer from the hub URL.")
|
||||
flag.StringVar(&listenIP, "listen-ip", "", "IP address to listen on.")
|
||||
flag.IntVar(&port, "port", 0, "Port to listen on.")
|
||||
flag.Parse()
|
||||
|
||||
if netName == "" {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if initURL != "" {
|
||||
mainInit(netName, initURL)
|
||||
return
|
||||
}
|
||||
|
||||
peer, err := NewPeer(netName, listenIP, uint16(port))
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create peer: %v", err)
|
||||
}
|
||||
|
||||
peer.Run()
|
||||
}
|
||||
|
||||
func mainInit(netName, initURL string) {
|
||||
if _, err := loadPeerConfig(netName); err == nil {
|
||||
log.Fatalf("Network is already initialized.")
|
||||
}
|
||||
|
||||
resp, err := http.Get(initURL)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to fetch data from hub: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
peerConfig := m.PeerConfig{}
|
||||
if err := json.Unmarshal(data, &peerConfig); err != nil {
|
||||
log.Fatalf("Failed to parse configuration: %v", err)
|
||||
}
|
||||
|
||||
if err := storePeerConfig(netName, peerConfig); err != nil {
|
||||
log.Fatalf("Failed to store configuration: %v", err)
|
||||
}
|
||||
|
||||
log.Print("Initialization successful.")
|
||||
}
|
26
peer/nonce.go
Normal file
26
peer/nonce.go
Normal file
@ -0,0 +1,26 @@
|
||||
package peer
|
||||
|
||||
import "unsafe"
|
||||
|
||||
func ParseNonceBytes(nb []byte, nonce *Nonce) {
|
||||
nonce.Counter = *(*uint64)(unsafe.Pointer(&nb[0]))
|
||||
nonce.SourceIP = nb[8]
|
||||
nonce.ViaIP = nb[9]
|
||||
nonce.DestIP = nb[10]
|
||||
nonce.StreamID = nb[11]
|
||||
nonce.PacketType = nb[12]
|
||||
}
|
||||
|
||||
func MarshalNonce(nonce Nonce, buf []byte) {
|
||||
*(*uint64)(unsafe.Pointer(&buf[0])) = nonce.Counter
|
||||
buf[8] = nonce.SourceIP
|
||||
buf[9] = nonce.ViaIP
|
||||
buf[10] = nonce.DestIP
|
||||
buf[11] = nonce.StreamID
|
||||
buf[12] = nonce.PacketType
|
||||
clear(buf[13:])
|
||||
}
|
||||
|
||||
func CounterTimestamp(counter uint64) int64 {
|
||||
return int64(counter >> 30)
|
||||
}
|
25
peer/nonce_test.go
Normal file
25
peer/nonce_test.go
Normal file
@ -0,0 +1,25 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMarshalParseNonce(t *testing.T) {
|
||||
nIn := Nonce{
|
||||
Counter: 3212,
|
||||
SourceIP: 34,
|
||||
ViaIP: 20,
|
||||
DestIP: 200,
|
||||
StreamID: 4,
|
||||
PacketType: 44,
|
||||
}
|
||||
|
||||
buf := make([]byte, NONCE_SIZE)
|
||||
MarshalNonce(nIn, buf)
|
||||
|
||||
nOut := Nonce{}
|
||||
ParseNonceBytes(buf, &nOut)
|
||||
if nIn != nOut {
|
||||
t.Fatal(nIn, nOut)
|
||||
}
|
||||
}
|
114
peer/peer-ifreader.go
Normal file
114
peer/peer-ifreader.go
Normal file
@ -0,0 +1,114 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/netip"
|
||||
"runtime/debug"
|
||||
"vppn/fasttime"
|
||||
)
|
||||
|
||||
func (peer *Peer) ifReader() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("stacktrace from panic: \n" + string(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
|
||||
var (
|
||||
n int
|
||||
destIP byte
|
||||
router = peer.router
|
||||
route *route
|
||||
iface = peer.iface
|
||||
nonce = Nonce{
|
||||
SourceIP: peer.ip,
|
||||
PacketType: PACKET_TYPE_DATA,
|
||||
StreamID: STREAM_DATA,
|
||||
}
|
||||
err error
|
||||
now uint64
|
||||
counterTS uint64
|
||||
counter uint64
|
||||
packet = make([]byte, BUFFER_SIZE)
|
||||
encrypted = make([]byte, BUFFER_SIZE)
|
||||
nonceBuf = make([]byte, NONCE_SIZE)
|
||||
toSend []byte
|
||||
signingKey = peer.signPrivKey
|
||||
|
||||
reqPool = make(chan udpWriteReq, 1024)
|
||||
writeChan = make(chan udpWriteReq, 1024)
|
||||
)
|
||||
|
||||
for range cap(reqPool) {
|
||||
reqPool <- udpWriteReq{Packet: make([]byte, BUFFER_SIZE)}
|
||||
}
|
||||
|
||||
go udpWriter(writeChan, peer.conn, reqPool)
|
||||
|
||||
for {
|
||||
n, err = iface.Read(packet[:BUFFER_SIZE])
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read from interface: %v", err)
|
||||
}
|
||||
|
||||
if n < 20 {
|
||||
log.Printf("Dropping small packet: %d", n)
|
||||
continue
|
||||
}
|
||||
|
||||
packet = packet[:n]
|
||||
|
||||
destIP = packet[19]
|
||||
route = router.GetRoute(destIP)
|
||||
if route == nil {
|
||||
log.Printf("Dropping packet for non-existent IP: %d", destIP)
|
||||
continue
|
||||
}
|
||||
|
||||
now = uint64(fasttime.Now())
|
||||
if counterTS < now {
|
||||
counterTS = now
|
||||
counter = now << 30
|
||||
}
|
||||
|
||||
counter++
|
||||
|
||||
nonce.Counter = counter
|
||||
nonce.ViaIP = route.ViaIP
|
||||
nonce.DestIP = destIP
|
||||
|
||||
MarshalNonce(nonce, nonceBuf)
|
||||
|
||||
encrypted = encryptPacket(route.EncSharedKey, nonceBuf, packet, encrypted)
|
||||
|
||||
if route.ViaIP != 0 {
|
||||
toSend = signPacket(signingKey, encrypted, packet)
|
||||
} else {
|
||||
toSend = encrypted
|
||||
}
|
||||
|
||||
req := <-reqPool
|
||||
req.Addr = route.Addr
|
||||
req.Packet = req.Packet[:len(toSend)]
|
||||
copy(req.Packet, toSend)
|
||||
|
||||
writeChan <- req
|
||||
}
|
||||
}
|
||||
|
||||
type udpWriteReq struct {
|
||||
Addr netip.AddrPort
|
||||
Packet []byte
|
||||
}
|
||||
|
||||
func udpWriter(in chan udpWriteReq, conn *net.UDPConn, reqPool chan udpWriteReq) {
|
||||
var err error
|
||||
for req := range in {
|
||||
if _, err = conn.WriteToUDPAddrPort(req.Packet, req.Addr); err != nil {
|
||||
log.Fatalf("Failed to write UDP packet: %v", err)
|
||||
}
|
||||
reqPool <- req
|
||||
}
|
||||
}
|
158
peer/peer-netreader.go
Normal file
158
peer/peer-netreader.go
Normal file
@ -0,0 +1,158 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"runtime/debug"
|
||||
"vppn/fasttime"
|
||||
)
|
||||
|
||||
func (peer *Peer) netReader() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("stacktrace from panic: \n" + string(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
|
||||
var (
|
||||
n int
|
||||
//srcAddr *net.UDPAddr
|
||||
nonce Nonce
|
||||
packet = make([]byte, BUFFER_SIZE)
|
||||
decrypted = make([]byte, BUFFER_SIZE)
|
||||
toWrite []byte
|
||||
route *route
|
||||
ok bool
|
||||
err error
|
||||
conn = peer.conn
|
||||
ip = peer.ip
|
||||
counters = [2][256]uint64{} // Counter by stream and IP.
|
||||
|
||||
ifaceChan = make(chan []byte, 1024)
|
||||
reqPool = make(chan []byte, 1024)
|
||||
)
|
||||
|
||||
for range cap(reqPool) {
|
||||
reqPool <- make([]byte, BUFFER_SIZE)
|
||||
}
|
||||
|
||||
go ifWriter(ifaceChan, peer.iface, reqPool)
|
||||
|
||||
NEXT_PACKET:
|
||||
|
||||
n, _, err = conn.ReadFromUDPAddrPort(packet[:BUFFER_SIZE])
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read UDP packet: %v", err)
|
||||
}
|
||||
|
||||
if n < NONCE_SIZE {
|
||||
log.Printf("Dropping short UDP packet: %d", n)
|
||||
goto NEXT_PACKET
|
||||
}
|
||||
|
||||
packet = packet[:n]
|
||||
|
||||
ParseNonceBytes(packet[n-NONCE_SIZE:], &nonce)
|
||||
|
||||
// Drop after 8 seconds.
|
||||
if CounterTimestamp(nonce.Counter) < fasttime.Now()-8 {
|
||||
log.Printf("Dropping old packet: %d", CounterTimestamp(nonce.Counter))
|
||||
goto NEXT_PACKET
|
||||
}
|
||||
|
||||
if nonce.StreamID > 1 {
|
||||
log.Printf("Dropping invalid stream ID: %v", nonce)
|
||||
goto NEXT_PACKET
|
||||
}
|
||||
|
||||
// Check source counter.
|
||||
if nonce.Counter <= counters[nonce.StreamID][nonce.SourceIP] {
|
||||
log.Printf("Dropping packet with bad counter: %v", nonce)
|
||||
goto NEXT_PACKET
|
||||
}
|
||||
|
||||
counters[nonce.StreamID][nonce.SourceIP] = nonce.Counter
|
||||
|
||||
route = peer.router.GetRoute(nonce.SourceIP)
|
||||
if route == nil {
|
||||
log.Printf("Dropping packet without route: %v", nonce)
|
||||
goto NEXT_PACKET
|
||||
}
|
||||
|
||||
switch ip {
|
||||
case nonce.DestIP:
|
||||
goto DECRYPT
|
||||
case nonce.ViaIP:
|
||||
goto VALIDATE_SIGNATURE
|
||||
default:
|
||||
log.Printf("Bad packet: %v", nonce)
|
||||
goto NEXT_PACKET
|
||||
}
|
||||
|
||||
DECRYPT:
|
||||
|
||||
decrypted, ok = decryptPacket(route.EncSharedKey, packet, decrypted)
|
||||
if !ok {
|
||||
log.Printf("Failed to decrypt packet: %v", nonce)
|
||||
goto NEXT_PACKET
|
||||
}
|
||||
|
||||
switch nonce.StreamID {
|
||||
case STREAM_DATA:
|
||||
goto WRITE_IFACE_DATA
|
||||
case STREAM_ROUTING:
|
||||
goto WRITE_ROUTING_PACKET
|
||||
default:
|
||||
log.Printf("Invalid stream ID: %d", nonce.StreamID)
|
||||
goto NEXT_PACKET
|
||||
}
|
||||
|
||||
WRITE_IFACE_DATA:
|
||||
|
||||
//toWrite = toWrite[:len(decrypted)]
|
||||
//copy(toWrite, decrypted)
|
||||
|
||||
toWrite = <-reqPool
|
||||
ifaceChan <- append(toWrite[:0], decrypted...)
|
||||
|
||||
goto NEXT_PACKET
|
||||
|
||||
WRITE_ROUTING_PACKET:
|
||||
|
||||
//peer.processRoutingPacket(decrypted)
|
||||
//peer.routeManager.ProcessPacket(decrypted)
|
||||
|
||||
goto NEXT_PACKET
|
||||
|
||||
VALIDATE_SIGNATURE:
|
||||
|
||||
// We don't forward twice.
|
||||
if route.Mediator {
|
||||
log.Printf("Dropping double-forward packet: %v", nonce)
|
||||
goto NEXT_PACKET
|
||||
}
|
||||
|
||||
decrypted, ok = openPacket(route.SignPubKey, packet, decrypted)
|
||||
if !ok {
|
||||
log.Printf("Failed to open signed packet: %v", nonce)
|
||||
goto NEXT_PACKET
|
||||
}
|
||||
|
||||
if _, err = conn.WriteToUDPAddrPort(decrypted, route.Addr); err != nil {
|
||||
log.Fatalf("Failed to forward packet: %v", err)
|
||||
}
|
||||
|
||||
goto NEXT_PACKET
|
||||
}
|
||||
|
||||
func ifWriter(in chan []byte, iface io.ReadWriteCloser, out chan []byte) {
|
||||
var err error
|
||||
for packet := range in {
|
||||
if _, err = iface.Write(packet); err != nil {
|
||||
log.Printf("Size: %d", len(packet))
|
||||
log.Fatalf("Failed to write to interface: %v", err)
|
||||
}
|
||||
out <- packet
|
||||
}
|
||||
}
|
67
peer/peer.go
Normal file
67
peer/peer.go
Normal file
@ -0,0 +1,67 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
)
|
||||
|
||||
type Peer struct {
|
||||
// Immutable data.
|
||||
ip byte // Last byte of IPv4 address.
|
||||
hubAddr string
|
||||
apiKey string
|
||||
|
||||
encPubKey []byte
|
||||
encPrivKey []byte
|
||||
signPubKey []byte
|
||||
signPrivKey []byte
|
||||
conn *net.UDPConn
|
||||
iface io.ReadWriteCloser
|
||||
|
||||
router *Router
|
||||
}
|
||||
|
||||
func NewPeer(netName, listenIP string, port uint16) (*Peer, error) {
|
||||
conf, err := loadPeerConfig(netName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
peer := &Peer{
|
||||
ip: conf.PeerIP,
|
||||
hubAddr: conf.HubAddress,
|
||||
apiKey: conf.APIKey,
|
||||
encPubKey: conf.EncPubKey,
|
||||
encPrivKey: conf.EncPrivKey,
|
||||
signPubKey: conf.SignPubKey,
|
||||
signPrivKey: conf.SignPrivKey,
|
||||
}
|
||||
|
||||
peer.router = NewRouter(conf)
|
||||
|
||||
port = determinePort(conf.Port, port)
|
||||
|
||||
conn, err := openUDPConn(listenIP, port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
peer.conn = conn
|
||||
|
||||
peer.iface, err = openInterface(conf.Network, conf.PeerIP, netName)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return peer, nil
|
||||
}
|
||||
|
||||
func (p *Peer) Run() {
|
||||
go p.netReader()
|
||||
p.ifReader()
|
||||
}
|
18
peer/router-ping.go
Normal file
18
peer/router-ping.go
Normal file
@ -0,0 +1,18 @@
|
||||
package peer
|
||||
|
||||
type RoutingPingReq struct {
|
||||
PeerIP byte
|
||||
Type byte // 0 => local, 1 => direct, 2 => Via
|
||||
Addr []byte
|
||||
Port int
|
||||
SentAt int64 // unix milli
|
||||
}
|
||||
|
||||
type RoutingPingResp struct {
|
||||
PeerIP byte
|
||||
Type byte // 0 => local, 1 => direct, 2 => Via
|
||||
Addr []byte
|
||||
Port int
|
||||
SentAt int64
|
||||
RecvdAt int64
|
||||
}
|
123
peer/router.go
Normal file
123
peer/router.go
Normal file
@ -0,0 +1,123 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"vppn/m"
|
||||
)
|
||||
|
||||
var zeroAddrPort netip.AddrPort
|
||||
|
||||
type routeInfo struct {
|
||||
Up bool
|
||||
Route route
|
||||
}
|
||||
|
||||
type Router struct {
|
||||
conf m.PeerConfig
|
||||
routes [256]*atomic.Pointer[routeInfo]
|
||||
}
|
||||
|
||||
func NewRouter(conf m.PeerConfig) *Router {
|
||||
rm := &Router{
|
||||
conf: conf,
|
||||
}
|
||||
|
||||
for i := range rm.routes {
|
||||
rm.routes[i] = &atomic.Pointer[routeInfo]{}
|
||||
rm.routes[i].Store(&routeInfo{})
|
||||
}
|
||||
|
||||
go rm.pollHub()
|
||||
return rm
|
||||
}
|
||||
|
||||
func (rm *Router) GetRoute(ip byte) *route {
|
||||
if route := rm.routes[ip].Load(); route != nil && route.Up {
|
||||
return &route.Route
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rm *Router) pollHub() {
|
||||
u, err := url.Parse(rm.conf.HubAddress)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse hub address %s: %v", rm.conf.HubAddress, err)
|
||||
}
|
||||
u.Path = "/peer/fetch-state/"
|
||||
|
||||
client := &http.Client{Timeout: 8 * time.Second}
|
||||
|
||||
req := &http.Request{
|
||||
Method: http.MethodGet,
|
||||
URL: u,
|
||||
Header: http.Header{},
|
||||
}
|
||||
req.SetBasicAuth("", rm.conf.APIKey)
|
||||
|
||||
rm._pollHub(client, req)
|
||||
|
||||
for range time.Tick(time.Minute) {
|
||||
rm._pollHub(client, req)
|
||||
}
|
||||
}
|
||||
|
||||
func (rm *Router) _pollHub(client *http.Client, req *http.Request) {
|
||||
var state m.NetworkState
|
||||
|
||||
log.Printf("Fetching peer state from %s...", rm.conf.HubAddress)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch peer state: %v", err)
|
||||
return
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
if err != nil {
|
||||
log.Printf("Failed to read body: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &state); err != nil {
|
||||
log.Printf("Failed to unmarshal response from hub: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for i, peer := range state.Peers {
|
||||
if peer == nil {
|
||||
continue
|
||||
}
|
||||
route := rm.routes[i].Load()
|
||||
rm.routes[i].Store(rm.updateRoute(route, peer))
|
||||
}
|
||||
}
|
||||
|
||||
func (rm *Router) updateRoute(routePtr *routeInfo, peer *m.Peer) *routeInfo {
|
||||
if peer == nil {
|
||||
return &routeInfo{}
|
||||
}
|
||||
|
||||
route := *routePtr
|
||||
|
||||
addr, ok := netip.AddrFromSlice(peer.IP)
|
||||
if !ok {
|
||||
return &routeInfo{}
|
||||
}
|
||||
|
||||
route.Up = true
|
||||
route.Route.Addr = netip.AddrPortFrom(addr, peer.Port)
|
||||
route.Route.Mediator = peer.Mediator
|
||||
route.Route.ViaIP = 0
|
||||
if len(route.Route.SignPubKey) == 0 {
|
||||
route.Route.SignPubKey = peer.SignPubKey
|
||||
route.Route.EncSharedKey = computeSharedKey(peer.EncPubKey, rm.conf.EncPrivKey)
|
||||
}
|
||||
|
||||
return &route
|
||||
}
|
156
peer/startup.go
Normal file
156
peer/startup.go
Normal file
@ -0,0 +1,156 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func determinePort(confPort, portFromCommandLine uint16) uint16 {
|
||||
if portFromCommandLine != 0 {
|
||||
return portFromCommandLine
|
||||
}
|
||||
if confPort != 0 {
|
||||
return confPort
|
||||
}
|
||||
return DEFAULT_PORT
|
||||
}
|
||||
|
||||
func openUDPConn(listenIP string, port uint16) (*net.UDPConn, error) {
|
||||
myAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", listenIP, port))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to construct UDP address: %w", err)
|
||||
}
|
||||
|
||||
return net.ListenUDP("udp", myAddr)
|
||||
}
|
||||
|
||||
func openInterface(network []byte, localIP byte, name string) (io.ReadWriteCloser, error) {
|
||||
if len(network) != 4 {
|
||||
return nil, fmt.Errorf("expected network to be 4 bytes, got %d", len(network))
|
||||
}
|
||||
ip := net.IPv4(network[0], network[1], network[2], localIP)
|
||||
|
||||
//////////////////////////
|
||||
// Create TUN Interface //
|
||||
//////////////////////////
|
||||
|
||||
tunFD, err := syscall.Open("/dev/net/tun", syscall.O_RDWR|unix.O_CLOEXEC, 0600)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open TUN device: %w", err)
|
||||
}
|
||||
|
||||
// New interface request.
|
||||
req, err := unix.NewIfreq(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new TUN interface request: %w", err)
|
||||
}
|
||||
|
||||
// Flags:
|
||||
//
|
||||
// IFF_NO_PI => don't add packet info data to packets sent to the interface.
|
||||
// IFF_TUN => create a TUN device handling IP packets.
|
||||
req.SetUint16(unix.IFF_NO_PI | unix.IFF_TUN)
|
||||
|
||||
err = unix.IoctlIfreq(tunFD, unix.TUNSETIFF, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to set TUN device settings: %w", err)
|
||||
}
|
||||
|
||||
// Name may not be exactly the same?
|
||||
name = req.Name()
|
||||
|
||||
/////////////
|
||||
// Set MTU //
|
||||
/////////////
|
||||
|
||||
// We need a socket file descriptor to set other options for some reason.
|
||||
sockFD, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_IP)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open socket: %w", err)
|
||||
}
|
||||
defer unix.Close(sockFD)
|
||||
|
||||
req, err = unix.NewIfreq(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create MTU interface request: %w", err)
|
||||
}
|
||||
|
||||
req.SetUint32(MTU)
|
||||
if err = unix.IoctlIfreq(sockFD, unix.SIOCSIFMTU, req); err != nil {
|
||||
return nil, fmt.Errorf("failed to set interface MTU: %w", err)
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// Set Queue Length //
|
||||
//////////////////////
|
||||
|
||||
req, err = unix.NewIfreq(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create IP interface request: %w", err)
|
||||
}
|
||||
|
||||
req.SetUint16(1000)
|
||||
if err = unix.IoctlIfreq(sockFD, unix.SIOCSIFTXQLEN, req); err != nil {
|
||||
return nil, fmt.Errorf("failed to set interface queue length: %w", err)
|
||||
}
|
||||
|
||||
/////////////////////
|
||||
// Set IP and Mask //
|
||||
/////////////////////
|
||||
|
||||
req, err = unix.NewIfreq(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create IP interface request: %w", err)
|
||||
}
|
||||
|
||||
if err := req.SetInet4Addr(ip.To4()); err != nil {
|
||||
return nil, fmt.Errorf("failed to set interface request IP: %w", err)
|
||||
}
|
||||
|
||||
if err = unix.IoctlIfreq(sockFD, unix.SIOCSIFADDR, req); err != nil {
|
||||
return nil, fmt.Errorf("failed to set interface IP: %w", err)
|
||||
}
|
||||
|
||||
// SET MASK - must happen after setting address.
|
||||
req, err = unix.NewIfreq(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create mask interface request: %w", err)
|
||||
}
|
||||
|
||||
if err := req.SetInet4Addr(net.IPv4(255, 255, 255, 0).To4()); err != nil {
|
||||
return nil, fmt.Errorf("failed to set interface request mask: %w", err)
|
||||
}
|
||||
|
||||
if err := unix.IoctlIfreq(sockFD, unix.SIOCSIFNETMASK, req); err != nil {
|
||||
return nil, fmt.Errorf("failed to set interface mask: %w", err)
|
||||
}
|
||||
|
||||
////////////////////////
|
||||
// Bring Interface Up //
|
||||
////////////////////////
|
||||
|
||||
req, err = unix.NewIfreq(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create up interface request: %w", err)
|
||||
}
|
||||
|
||||
// Get current flags.
|
||||
if err = unix.IoctlIfreq(sockFD, unix.SIOCGIFFLAGS, req); err != nil {
|
||||
return nil, fmt.Errorf("failed to get interface flags: %w", err)
|
||||
}
|
||||
|
||||
flags := req.Uint16() | unix.IFF_UP | unix.IFF_RUNNING
|
||||
|
||||
// Set UP flag / broadcast flags.
|
||||
req.SetUint16(flags)
|
||||
if err = unix.IoctlIfreq(sockFD, unix.SIOCSIFFLAGS, req); err != nil {
|
||||
return nil, fmt.Errorf("failed to set interface up: %w", err)
|
||||
}
|
||||
|
||||
return os.NewFile(uintptr(tunFD), "tun"), nil
|
||||
}
|
22
peer/types.go
Normal file
22
peer/types.go
Normal file
@ -0,0 +1,22 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
type route struct {
|
||||
Mediator bool // Route is via a mediator.
|
||||
SignPubKey []byte
|
||||
EncSharedKey []byte // Shared key for encoding / decoding packets.
|
||||
Addr netip.AddrPort // Address to send to.
|
||||
ViaIP byte // If != 0, this is a forwarding address.
|
||||
}
|
||||
|
||||
type Nonce struct {
|
||||
Counter uint64
|
||||
SourceIP byte
|
||||
ViaIP byte
|
||||
DestIP byte
|
||||
StreamID byte // The stream, see STREAM_* constants
|
||||
PacketType byte // The packet type. See PACKET_* constants.
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user