pnyx/server.go

157 lines
4.0 KiB
Go

2024-04-03 18:52:04 -06:00
package pnyx
import (
"encoding/binary"
"errors"
"fmt"
"net"
"os"
"sync/atomic"
"github.com/google/uuid"
)
const (
SERVER_UDP_BUFFER_SIZE = 2048
PUBKEY_LENGTH = 32
ECDH_LENGTH = 32
SIGNATURE_LENGTH = 64
SESSION_OPEN_LENGTH = PUBKEY_LENGTH + ECDH_LENGTH + SIGNATURE_LENGTH
)
type PacketType uint16
const (
SESSION_OPEN PacketType = iota
SESSION_AUTHENTICATE
SESSION_CONNECT
SESSION_CLOSE
SESSION_CLOSED
SESSION_DATA
)
/*
Session Flow:
1. Client send a SESSION_OPEN packet to the server with first half for ECDH and it's public key
2. Server responds with an SESSION_AUTHENTICATE packet with the server's ECDH half and public key
aside - at this point if the client and server both hold the private parts of their public keys,
they both hold the same ECDH secret which they put in a KDF to generate a symmetric session key
aside - at this point the server creates the session in memory, but there is no return address associated
with it yet
3. Client sends a SESSION_CONNECT packet to the server with the session ID in cleartext and the
return address hashed with the key(to prove the return address has not been modified without the key)
4. Server adds the return address to the session info, and maps the address to the session for future packets
5. Server sends the HELLO packet to the client encrypted by the session key
If a client disconnects at any point and gets a new return address:
1. Client sends a SESSION_CONNECT packet to the server from the new socket
2. Server removes the old return address, and fills/maps the new return address
If a client wants to gracefully disconnect and notify the server to close the session:
1. Client sends a SESSION_CLOSE
2. Server responds with SESSION_CLOSED
Session Packets:
1. SESSION_OPEN
Payload is CLIENT_PUBKEY + ECDH_HALF + SIGNATURE
3. SESSION_CONNECT
4. SESSION_CLOSE
5. SESSION_CLOSED
*/
type SessionID uuid.UUID
type ClientID uuid.UUID
type Connection struct {
state string
session SessionID
}
type Session struct {
state string
client ClientID
}
type Server struct {
active atomic.Bool
connection *net.UDPConn
stopped chan error
connections map[string]SessionID
sessions map[SessionID]Session
}
func NewServer() *Server {
server := &Server{
connection: nil,
active: atomic.Bool{},
stopped: make(chan error, 0),
}
server.active.Store(false)
return server
}
func (server *Server) Log(format string, fields ...interface{}) {
fmt.Fprint(os.Stderr, fmt.Sprintf(format, fields...) + "\n")
}
func(server *Server) Stop() error {
was_active := server.active.CompareAndSwap(true, false)
if was_active {
err := server.connection.Close()
if err != nil {
return err
}
return <-server.stopped
} else {
return fmt.Errorf("Called stop func on stopped server")
}
}
func(server *Server) run() {
server.Log("Started server on %s", server.connection.LocalAddr())
var buf [SERVER_UDP_BUFFER_SIZE]byte
for true {
read, addr, err := server.connection.ReadFromUDP(buf[:])
if err == nil {
var packet_type PacketType = PacketType(binary.BigEndian.Uint16(buf[0:2]))
switch packet_type {
default:
server.Log("Unhandled packet type 0x%04x from %s: %+v", packet_type, addr, buf[:read])
}
} else if errors.Is(err, net.ErrClosed) {
server.Log("UDP_CLOSE: %s", server.connection.LocalAddr())
break
} else {
server.Log("UDP_READ_ERROR: %s", err)
}
}
server.Log("Shut down server on %s", server.connection.LocalAddr())
server.stopped <- nil
}
func(server *Server) Start(listen string) error {
was_inactive := server.active.CompareAndSwap(false, true)
if was_inactive == false {
return fmt.Errorf("Server already active")
}
address, err := net.ResolveUDPAddr("udp", listen)
if err != nil {
server.active.Store(false)
return err
}
server.connection, err = net.ListenUDP("udp", address)
if err != nil {
server.active.Store(false)
return fmt.Errorf("Failed to create udp server: %w", err)
}
go server.run()
return nil
}