laserproxy: init

Change-Id: I1900be6eea2d702548279176d796c58c34952dc8
changes/58/358/1
q3k 2020-07-30 20:48:48 +02:00
parent be89707ce1
commit de0330a07c
7 changed files with 515 additions and 0 deletions

View File

@ -0,0 +1,23 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "go_default_library",
srcs = [
"locker.go",
"main.go",
"proxy.go",
],
importpath = "code.hackerspace.pl/hscloud/hswaw/laserproxy",
visibility = ["//visibility:private"],
deps = [
"//go/mirko:go_default_library",
"//hswaw/laserproxy/tpl:go_default_library",
"@com_github_golang_glog//:go_default_library",
],
)
go_binary(
name = "laserproxy",
embed = [":go_default_library"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,17 @@
hswaw ruida laser proxy
=======================
This is a layer 7 proxy to access the Warsaw Hackerspace laser from the main LAN/wifi.
For more information about actually accessing the lasercutter in the space, see [the wiki entry](https://wiki.hackerspace.pl/infra:tools:lasercutter). The rest of this file will describe the software itself.
Architecture
------------
The laserproxy software is a single Go binary that runs on Customs, which has access to both the Hackerspace LAN and the laser network. It proxies UDP traffic from lasercutter users to the lasercutter itself.
Only one user is allowed at a time - to implement this mutual exclusion, a Locker worker manages a single (address, note) tuple that is currently allowed to proxy traffic, and to which return traffic is forwarded back. The Locker also maintians a deadline. After the deadline expires, the lock is automatically released. A user can also release their lock ahead of time.
A lock is taken through a web interface by the user that wants to access the laser.
When a lock is taken, the Locker will notify a Proxy worker about this address. The Proxy will then perform the UDP proxying. As traffic is proxied, the Proxy will send bump updates to the Locker to extend the lock deadline.

131
hswaw/laserproxy/locker.go Normal file
View File

@ -0,0 +1,131 @@
package main
import (
"context"
"time"
"github.com/golang/glog"
)
type lockCtrl struct {
getCurrent *lockCtrlGetCurrent
take *lockCtrlTake
release *lockCtrlRelease
subscribe *lockCtrlSubscribe
bump *lockCtrlBump
}
type lockCtrlGetCurrent struct {
res chan *lockResCurrent
}
type lockCtrlTake struct {
note string
addr string
prev string
res chan bool
}
type lockCtrlRelease struct {
addr string
force bool
res chan struct{}
}
type lockCtrlSubscribe struct {
subscriber chan *lockUpdate
}
type lockCtrlBump struct {
addr string
}
type lockResCurrent struct {
note string
addr string
deadline time.Time
}
type lockUpdate struct {
note string
addr string
}
func (s *service) runLocker(ctx context.Context, ctrlC chan *lockCtrl) {
glog.Infof("Locker starting...")
var curNote string
var curAddr string
var curDeadline time.Time
var subscribers []chan *lockUpdate
notify := func() {
for _, sub := range subscribers {
go func() {
sub <- &lockUpdate{
note: curNote,
addr: curAddr,
}
}()
}
}
t := time.NewTicker(5 * time.Second)
defer t.Stop()
for {
select {
case <-ctx.Done():
err := ctx.Err()
glog.Errorf("Locker stoppped: %v", err)
return
case <-t.C:
if curAddr != "" && time.Now().After(curDeadline) {
glog.Infof("Expiring lock")
curAddr = ""
curNote = ""
notify()
}
case ctrl := <-ctrlC:
switch {
case ctrl.take != nil:
won := false
if curAddr == ctrl.take.prev {
won = true
curNote = ctrl.take.note
curAddr = ctrl.take.addr
curDeadline = time.Now().Add(15 * time.Minute)
notify()
glog.Infof("Lock taken by %q %q", curNote, curAddr)
}
go func() {
ctrl.take.res <- won
}()
case ctrl.release != nil:
if curAddr == ctrl.release.addr || ctrl.release.force {
curAddr = ""
curNote = ""
notify()
glog.Infof("Lock relased")
}
go func() {
ctrl.release.res <- struct{}{}
}()
case ctrl.getCurrent != nil:
go func() {
ctrl.getCurrent.res <- &lockResCurrent{
note: curNote,
addr: curAddr,
deadline: curDeadline,
}
}()
case ctrl.bump != nil:
if curAddr != "" {
curDeadline = time.Now().Add(15 * time.Minute)
}
case ctrl.subscribe != nil:
subscribers = append(subscribers, ctrl.subscribe.subscriber)
}
}
}
}

205
hswaw/laserproxy/main.go Normal file
View File

@ -0,0 +1,205 @@
package main
import (
"context"
"flag"
"fmt"
"html/template"
"net"
"net/http"
"math/rand"
"strings"
"time"
"github.com/golang/glog"
mirko "code.hackerspace.pl/hscloud/go/mirko"
"code.hackerspace.pl/hscloud/hswaw/laserproxy/tpl"
)
var (
flagLaserAddress = "10.11.0.10"
flagLaserNetworkAddress = "10.11.0.1"
flagClientNetworkAddress = "10.8.1.2"
flagWebAddress = "127.0.0.1:8080"
tplIndex = template.Must(template.New("index.html").Parse(tpl.MustAssetString("index.html")))
)
type service struct {
lockCtrl chan *lockCtrl
}
func main() {
flag.StringVar(&flagLaserAddress, "laser_address", flagLaserAddress, "Address of Ruida controller on laser network")
flag.StringVar(&flagLaserNetworkAddress, "laser_network", flagLaserNetworkAddress, "Address of proxy on laser network")
flag.StringVar(&flagClientNetworkAddress, "client_network", flagClientNetworkAddress, "Address of proxy on client network")
flag.StringVar(&flagWebAddress, "web_address", flagWebAddress, "Address and port to listen on for public web connections")
flag.Parse()
m := mirko.New()
if err := m.Listen(); err != nil {
glog.Exitf("Could not listen: %v", err)
}
lisLaser, err := net.ListenPacket("udp", fmt.Sprintf("%s:40200", flagLaserNetworkAddress))
if err != nil {
glog.Fatalf("could not listen on laser network: %v", err)
}
defer lisLaser.Close()
lisClient, err := net.ListenPacket("udp", fmt.Sprintf("%s:50200", flagClientNetworkAddress))
if err != nil {
glog.Fatalf("could not listen on client network: %v", err)
}
defer lisClient.Close()
laserIP := net.ParseIP(flagLaserAddress)
if laserIP == nil {
glog.Fatalf("could not parse laser IP address %q", flagLaserAddress)
}
laserAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:50200", laserIP.String()))
if err != nil {
glog.Fatalf("could not make laser UDP address: %v", err)
}
ctx := m.Context()
s := &service{
lockCtrl: make(chan *lockCtrl),
}
updates := make(chan *lockUpdate)
go s.runProxy(ctx, updates, laserAddr, lisLaser, lisClient)
go s.runLocker(ctx, s.lockCtrl)
s.lockCtrl <- &lockCtrl{
subscribe: &lockCtrlSubscribe{
subscriber: updates,
},
}
mux := http.NewServeMux()
mux.HandleFunc("/", s.handlerIndex)
mux.HandleFunc("/take", s.handlerTake)
mux.HandleFunc("/release", s.handlerRelease)
mux.HandleFunc("/force", s.handlerForce)
httpSrv := &http.Server{Addr: flagWebAddress, Handler: mux}
glog.Infof("Listening for web connections on %q...", flagWebAddress)
go func() {
if err := httpSrv.ListenAndServe(); err != nil {
glog.Error(err)
}
}()
if err := m.Serve(); err != nil {
glog.Exitf("Could not run: %v", err)
}
glog.Info("Running.")
<-m.Done()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
httpSrv.Shutdown(ctx)
}
var (
sampleNames = []string{
"enleth", "radex", "qdot", "hans acker", "makłowicz",
}
)
func remoteAddr(r *http.Request) string {
if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
return strings.Split(fwd, ":")[0]
}
return strings.Split(r.RemoteAddr, ":")[0]
}
func (s *service) handlerIndex(w http.ResponseWriter, r *http.Request) {
res := make(chan *lockResCurrent)
s.lockCtrl <- &lockCtrl{
getCurrent: &lockCtrlGetCurrent{
res: res,
},
}
cur := <-res
err := tplIndex.Execute(w, struct {
You bool
CurrentAddress string
CurrentNote string
CurrentDeadline string
SampleName string
}{
You: cur.addr == remoteAddr(r),
CurrentAddress: cur.addr,
CurrentNote: cur.note,
CurrentDeadline: fmt.Sprintf("%d minute(s)", int(cur.deadline.Sub(time.Now()).Minutes())),
SampleName: sampleNames[rand.Intn(len(sampleNames))],
})
if err != nil {
glog.Errorf("rendering template: %v", err)
}
}
func (s *service) handlerTake(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
return
}
r.ParseForm()
who := r.Form.Get("who")
prev := r.Form.Get("prev")
if who == "" {
fmt.Fprintf(w, "sorry, who are you? please specify a name")
return
}
res := make(chan bool)
take := &lockCtrlTake{
note: who,
addr: remoteAddr(r),
prev: prev,
res: res,
}
s.lockCtrl <- &lockCtrl{
take: take,
}
won := <-res
if won {
http.Redirect(w, r, "/", 302)
} else {
fmt.Fprintf(w, "lock not taken")
}
}
func (s *service) handlerRelease(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
return
}
res := make(chan struct{})
s.lockCtrl <- &lockCtrl{
release: &lockCtrlRelease{
addr: remoteAddr(r),
res: res,
},
}
<-res
http.Redirect(w, r, "/", 302)
}
func (s *service) handlerForce(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
return
}
res := make(chan struct{})
s.lockCtrl <- &lockCtrl{
release: &lockCtrlRelease{
force: true,
res: res,
},
}
<-res
http.Redirect(w, r, "/", 302)
}

69
hswaw/laserproxy/proxy.go Normal file
View File

@ -0,0 +1,69 @@
package main
import (
"context"
"net"
"strings"
"github.com/golang/glog"
)
type packetFrom struct {
addr net.Addr
data []byte
}
func (s *service) runProxy(ctx context.Context, updates chan *lockUpdate, laserAddr net.Addr, laserNet, clientNet net.PacketConn) {
glog.Infof("Proxy starting... (laser: %v, laser network: %v, client network: %v)", laserAddr, laserNet, clientNet)
pipe := func(conn net.PacketConn, C chan *packetFrom) {
for {
buf := make([]byte, 1500)
n, addr, err := conn.ReadFrom(buf)
if err != nil {
glog.Errorf("pipe failed: %v", err)
}
C <- &packetFrom{ addr, buf[:n] }
}
}
laserC := make(chan *packetFrom)
go pipe(laserNet, laserC)
clientC := make(chan *packetFrom)
go pipe(clientNet, clientC)
var allowedClient string
var curClient *net.Addr
for {
select {
case <-ctx.Done():
err := ctx.Err()
glog.Errorf("Proxy stopped: %v", err)
return
case u := <-updates:
allowedClient = u.addr
glog.Infof("New allowed client: %q", allowedClient)
case p := <-laserC:
s.lockCtrl <- &lockCtrl{
bump: &lockCtrlBump{},
}
if curClient == nil {
glog.Warningf("Packet from laser without client connected, dropping.")
break
}
clientNet.WriteTo(p.data, *curClient)
case p := <-clientC:
s.lockCtrl <- &lockCtrl{
bump: &lockCtrlBump{},
}
if strings.Split(p.addr.String(), ":")[0] == allowedClient {
curClient = &p.addr
laserNet.WriteTo(p.data, laserAddr)
} else {
glog.Infof("Rejecting packet from %s", p.addr.String())
}
}
}
}

View File

@ -0,0 +1,18 @@
load("@io_bazel_rules_go//extras:bindata.bzl", "bindata")
load("@io_bazel_rules_go//go:def.bzl", "go_library")
bindata(
name = "tpl_bindata",
srcs = glob(["*"]),
extra_args = ["."],
package = "tpl",
)
go_library(
name = "go_default_library",
srcs = [
":tpl_bindata", # keep
],
importpath = "code.hackerspace.pl/hscloud/hswaw/laserproxy/tpl", # keep
visibility = ["//hswaw/laserproxy:__subpackages__"],
)

View File

@ -0,0 +1,52 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>laserproxy</title>
</head>
<body>
<h1>warsaw hackerspace SOKÓŁ proxy</h1>
{{ if eq .CurrentAddress "" }}
<p>
Currently <b>not</b> in use by anyone. Wanna lase something?
</p>
<form action="/take" method="POST">
I am <input id="persist" type="text" name="who" placeholder="{{ .SampleName }}" /> and want to <input type="submit" value="use the laser" /> over the network.
</form>
{{ else if .You }}
<p>
Currently in use by <b>you</b> ({{ .CurrentNote }}, {{ .CurrentAddress }}). <b>Expires in {{ .CurrentDeadline }}.</b> This deadline will automatically extend as long as the laser is actively used.
</p>
<p>
To cut something, use LightBurn, and point it as <b>10.8.1.2</b> (as a 'Ruida' Ethernet/LAN controller).
</p>
<form action="/release" method="POST">
<input type="submit" value="I'm done with the laser." />
</form>
{{ else }}
<p>
Currently in use by '{{ .CurrentNote }}' ({{ .CurrentAddress }}). <b>Expires in {{ .CurrentDeadline }}</b>.
</p>
<form action="/force" method="POST">
I need to use the laser now and I can't ask the current user ({{ .CurrentNote }}) to release the lock. <input type="submit" value="I want to forcefully release the lock" />, and am aware of the possible consequences of that.
</form>
{{ end }}
<p>
<b>Confused by this?</b> See our <a href="https://wiki.hackerspace.pl/infra:tools:lasercutter">wiki entry about how to use the laser</a>.
</p>
<script>
let element = document.querySelector("#persist");
if (element !== null) {
let existing = localStorage.getItem("hacker");
if (existing !== "" && existing !== null) {
element.value = existing;
}
element.addEventListener('change', (event) => {
let value = event.target.value;
localStorage.setItem("hacker", value);
});
}
</script>
</body>
</html>