142 lines
3.7 KiB
Go
142 lines
3.7 KiB
Go
|
package pnyx
|
||
|
|
||
|
import (
|
||
|
"encoding/binary"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"net"
|
||
|
"os"
|
||
|
"sync/atomic"
|
||
|
|
||
|
"github.com/google/uuid"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
SERVER_UDP_BUFFER_SIZE = 2048
|
||
|
)
|
||
|
|
||
|
/*
|
||
|
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
|
||
|
}
|