From 228267760921c75a17756f6e115e14e7157de6ea Mon Sep 17 00:00:00 2001 From: NHAS Date: Tue, 14 May 2024 19:54:36 +1200 Subject: [PATCH 01/19] Update some definitions --- internal/data/user.go | 2 +- internal/webserver/authenticators/oidc.go | 6 +++--- internal/webserver/web.go | 3 --- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/internal/data/user.go b/internal/data/user.go index 5c182137..ca6c247b 100644 --- a/internal/data/user.go +++ b/internal/data/user.go @@ -24,7 +24,7 @@ func (um *UserModel) GetID() [20]byte { return sha1.Sum([]byte(um.Username)) } -// Make sure that the attempts is always incremented first to stop race condition attacks +// IncrementAuthenticationAttempt Make sure that the attempts is always incremented first to stop race condition attacks func IncrementAuthenticationAttempt(username, device string) error { return doSafeUpdate(context.Background(), deviceKey(username, device), false, func(gr *clientv3.GetResponse) (value string, err error) { diff --git a/internal/webserver/authenticators/oidc.go b/internal/webserver/authenticators/oidc.go index b31bf2fb..f0050f86 100644 --- a/internal/webserver/authenticators/oidc.go +++ b/internal/webserver/authenticators/oidc.go @@ -178,7 +178,7 @@ func (o *Oidc) AuthorisationAPI(w http.ResponseWriter, r *http.Request) { } // Rather ugly way of converting []interface{} into []string{} - groups := []string{} + var groups []string for i := range groupsIntf { conv, ok := groupsIntf[i].(string) if !ok { @@ -245,10 +245,10 @@ func (o *Oidc) AuthorisationAPI(w http.ResponseWriter, r *http.Request) { rp.CodeExchangeHandler(rp.UserinfoCallback(marshalUserinfo), o.provider)(w, r) } -func (o *Oidc) MFAPromptUI(w http.ResponseWriter, r *http.Request, username, ip string) { +func (o *Oidc) MFAPromptUI(w http.ResponseWriter, r *http.Request, _, _ string) { rp.AuthURLHandler(o.state, o.provider)(w, r) } -func (o *Oidc) RegistrationUI(w http.ResponseWriter, r *http.Request, username, ip string) { +func (o *Oidc) RegistrationUI(w http.ResponseWriter, r *http.Request, _, _ string) { o.RegistrationAPI(w, r) } diff --git a/internal/webserver/web.go b/internal/webserver/web.go index 100d0899..939c1af1 100644 --- a/internal/webserver/web.go +++ b/internal/webserver/web.go @@ -62,9 +62,6 @@ func Teardown() { func Start(errChan chan<- error) error { //https://blog.cloudflare.com/exposing-go-on-the-internet/ tlsConfig := &tls.Config{ - // Causes servers to use Go's default ciphersuite preferences, - // which are tuned to avoid attacks. Does nothing on clients. - PreferServerCipherSuites: true, // Only use curves which have assembly implementations CurvePreferences: []tls.CurveID{ tls.CurveP256, From 28855d0eb336778ddc059cac2840afb09bc307c6 Mon Sep 17 00:00:00 2001 From: NHAS Date: Tue, 14 May 2024 20:02:21 +1200 Subject: [PATCH 02/19] Add device association in authorisation function, remove old comment --- internal/data/devices.go | 6 +++++- internal/data/init.go | 4 ---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/data/devices.go b/internal/data/devices.go index d86cec91..426b343d 100644 --- a/internal/data/devices.go +++ b/internal/data/devices.go @@ -23,6 +23,8 @@ type Device struct { Attempts int Active bool Authorised time.Time + + AssociatedNode string } func (d Device) String() string { @@ -32,7 +34,7 @@ func (d Device) String() string { authorised = d.Authorised.Format(time.DateTime) } - return fmt.Sprintf("device[%s:%s][active: %t, attempts: %d, authorised: %s]", d.Username, d.Address, d.Active, d.Attempts, authorised) + return fmt.Sprintf("device[%s:%s:%s][active: %t, attempts: %d, authorised: %s]", d.Username, d.Address, d.AssociatedNode, d.Active, d.Attempts, authorised) } func UpdateDeviceEndpoint(address string, endpoint *net.UDPAddr) error { @@ -58,6 +60,7 @@ func UpdateDeviceEndpoint(address string, endpoint *net.UDPAddr) error { } device.Endpoint = endpoint + device.AssociatedNode = GetServerID() b, _ := json.Marshal(device) @@ -108,6 +111,7 @@ func AuthoriseDevice(username, address string) error { return "", errors.New("account is locked") } + device.AssociatedNode = GetServerID() device.Authorised = time.Now() device.Attempts = 0 diff --git a/internal/data/init.go b/internal/data/init.go index 3cfaa12f..bd41c392 100644 --- a/internal/data/init.go +++ b/internal/data/init.go @@ -24,10 +24,6 @@ import ( "go.etcd.io/etcd/server/v3/embed" ) -// TODO Most of the methods in this package need to change to prevent race conditions from breaking the cluster -// The way we're going to do this is each get method will return the etcd key revision. If a user tries to make a change using an older revision then that will be an error -// Or a prompt on the admin ui - var ( etcd *clientv3.Client etcdServer *embed.Etcd From 2959f44f27ec68f564374aa751a73e73c5002ae3 Mon Sep 17 00:00:00 2001 From: NHAS Date: Tue, 14 May 2024 20:03:08 +1200 Subject: [PATCH 03/19] Add goland settings --- .idea/.gitignore | 8 ++++++++ .idea/modules.xml | 8 ++++++++ .idea/vcs.xml | 6 ++++++ .idea/wag.iml | 9 +++++++++ 4 files changed, 31 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 .idea/wag.iml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..13566b81 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..1d3a2a3d --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/wag.iml b/.idea/wag.iml new file mode 100644 index 00000000..5e764c4f --- /dev/null +++ b/.idea/wag.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file From 889a816d6b1cb35c7cb6cec737773a8ba6e485f4 Mon Sep 17 00:00:00 2001 From: NHAS Date: Tue, 14 May 2024 21:47:22 +1200 Subject: [PATCH 04/19] Update docs and ascii diagram, add node association map --- internal/router/xdp.c | 173 ++++++++++++++++++++++++++---------------- 1 file changed, 109 insertions(+), 64 deletions(-) diff --git a/internal/router/xdp.c b/internal/router/xdp.c index e2e1faf1..06800019 100644 --- a/internal/router/xdp.c +++ b/internal/router/xdp.c @@ -14,34 +14,33 @@ char __license[] SEC("license") = "Dual MIT/GPL"; /* A massive oversimplifcation of what is in this file. - - ┌───────────────────────────────┐ ┌───────────────────────────────────┐ - │ Inactivity Timeout │ │ Devices │ - │ │ │ map │ - │ uint64 (minutes) │ │ key: ipv4 (u32) │ - │ │ │ val: sizeof(struct device) │ - └───────────────────────────────┘ │ │ │ - └─────────────────┼─────────────────┘ - │ - ┌─────────────▼──────────────┐ - │ device struct │ - ┌─────────────────────────────────────┐ │ │ - │ User │◄─────────────┼─ userid char[20] │ - │ │ │ sessionExpiry uint64 │ - ├─────────────────────────────────────┤ │ lastPacketTime uint64 │ - │ AccountLocked │ │ deviceLock uint32 │ - │ uint32 │ └────────────────────────────┘ - ├─────────────────────────────────────┤ - │ Public Routes LPM │ - │ key uint32 │ ┌─────────────────────────────┐ - │ value policies[128]─────────┼───────┐ │ policy struct │ - │ │ │ │ policy_type uint16 │ - ├─────────────────────────────────────┤ ├────►│ lower_port uint16 │ - │ MFA Routes LPM │ │ │ upper_port uint16 │ - │ key uint32 │ │ │ proto uint16 │ - │ value policies[128] ────────┼───────┘ │ │ - │ │ └─────────────────────────────┘ - └─────────────────────────────────────┘ + ┌──────────────────────────────┐ + │ Devices │ + │ map │ + │ key: ipv4 (u32) │ + │ val: sizeof(struct device) │ + │ │ │ + ┌────────────────────────────────────────────┐ └──────────────┼───────────────┘ + │ Policies │ │ + │ │ ┌─────────────▼──────────────┐ + │ map │ │ device struct │ + │ │ │ │ + │ key: userid char[20] ◄────────────────────┼─────────────────┼─ userid char[20] │ + │ val: Max Polices * sizeof(struct device) │ │ sessionExpiry uint64 │ + │ │ │ │ lastPacketTime uint64 │ + └──────────────────────┼─────────────────────┘ │ associatedNode uint64 │ + │ │ │ + │ └────────────────────────────┘ + Max Policies + │ + ┌──────────────▼──────────────┐ + │ policy struct │ ┌────────────────────┐ ┌─────────────────────────┐ + │ policy_type uint16 │ │ Inactivity Timeout │ │ Associated Node │ + │ lower_port uint16 │ │ │ │ │ + │ upper_port uint16 │ │ Array │ │ Array │ + │ proto uint16 │ │ │ │ │ + │ │ │ uint64 (minutes) │ │ uint64 (etcd node id) │ + └─────────────────────────────┘ └────────────────────┘ └─────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Packet Flow │ @@ -81,41 +80,60 @@ A massive oversimplifcation of what is in this file. │ │ │ │ │ │ │ ┌───────────▼─────────────┐ │ -│ │ Lookup User │ │ +│ │ │ │ │ │ │ ┌────────┐ │ -│ │ user = users( │ not_found(userid) │ │ │ -│ │ userid │ ───────────────────────────────────────────────► DROP │ │ -│ │ ) │ │ │ │ -│ │ │ └────────┘ │ -│ └───────────┬─────────────┘ │ +│ │ Check User Exists │ not_found(userid) │ │ │ +│ │ │ ───────────────────────────────────────────────► DROP │ │ +│ │ │ │ │ │ +│ └───────────┬─────────────┘ └────────┘ │ +│ │ │ +│ │ │ +│ node_id : uint64 │ │ │ │ │ +│ ▼ │ +│ ┌──────────────────────┐ │ +│ │ │ │ +│ │ Check │ device not associated with current ┌────────┐ │ +│ │ │ node │ │ │ +│ │ Node ID │ ───────────────────────────────────────────────► DROP │ │ +│ │ = │ │ │ │ +│ │ Peer Associated ID │ └────────┘ │ +│ │ │ │ +│ └──────────┬───────────┘ │ │ │ │ -│ device.LastPacketTime : u64 │ │ -│ dst_ip : u32 │ │ │ │ │ │ │ │ │ │ │ -│ ┌────────────▼────────────┐ ┌────────┐ │ -│ │ Routes │ matches neither public or mfa routes │ │ │ -│ │ Check │ ────────────────────────────────────────────────► DROP │ │ -│ └────────────┬────────────┘ │ │ │ -│ │ └────────┘ │ │ │ │ -│ policies struct policy[128] │ │ +│ ▼ │ +│ ┌──────────────────────┐ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ Check Policies │ │ +│ │ │◄─────────────────────┐ │ +│ │ │ │ │ +│ │ │ │ │ +│ └──────────┬──────┬────┘ Check all policies │ │ +│ │ │ 128 MAX │ │ +│ │ │ │ │ +│ │ └───────────────────────────┘ │ │ │ │ │ │ │ -│ ┌────────┴─────────┐ │ -│ │ │ ┌────────┐ │ -│ │ Check Policies │ no_match(polices,port,proto) │ │ │ -│ │ │ ───────────────────────────────────────────────► DROP │ │ -│ └────────┬─────────┘ │ │ │ -│ │ └────────┘ │ │ │ │ -│ ┌────▼────┐ │ -│ │ │ │ -│ │ PASS │ │ -│ │ │ │ -│ └─────────┘ │ +│ │ If user policies allow access to dst ip │ +│ │ and │ +│ │ (either user is authorized │ +│ │ or │ +│ │ the route is public/always allowed) │ +│ │ │ +│ │ │ +│ ┌────▼───┐ │ +│ │ │ │ +│ │ PASS │ │ +│ │ │ │ +│ └────────┘ │ +│ │ │ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ @@ -273,6 +291,17 @@ struct icmphdr } un; }; +struct ip +{ + __u32 src_ip; + __u16 src_port; + + __u32 dst_ip; + __u16 dst_port; + + __u32 proto; +}; + struct device { __u64 sessionExpiry; @@ -284,18 +313,10 @@ struct device __u32 PAD; -} __attribute__((__packed__)); - -struct ip -{ - __u32 src_ip; - __u16 src_port; + __u64 associatedNode; - __u32 dst_ip; - __u16 dst_port; +} __attribute__((__packed__)); - __u32 proto; -}; struct bpf_map_def SEC("maps") devices = { .type = BPF_MAP_TYPE_HASH, @@ -352,6 +373,15 @@ struct bpf_map_def SEC("maps") inactivity_timeout_minutes = { .map_flags = 0, }; +// A single variable that contains the node ID +struct bpf_map_def SEC("maps") node_Id = { + .type = BPF_MAP_TYPE_ARRAY, + .max_entries = 1, + .key_size = sizeof(__u32), + .value_size = sizeof(__u64), + .map_flags = 0, +}; + /* Attempt to parse the IPv4 source address from the packet. Returns 0 if there is no IPv4 header field; otherwise returns non-zero. @@ -479,8 +509,23 @@ static __always_inline int conntrack(struct ip *ip_info) return 0; } - // // Our userland defined inactivity timeout + + // General index used to get things out of the map arrays __u32 index = 0; + + __u64 *current_node_id = bpf_map_lookup_elem(&node_Id, &index); + if (current_node_id == NULL) + { + return 0; + } + + // If the traffic comes from a peer that we are not associated with, i.e traffic is coming to a node who has not talked to this peer before + // kill it + if(*current_node_id != current_device->associatedNode) { + return 0; + } + + __u64 *inactivity_timeout = bpf_map_lookup_elem(&inactivity_timeout_minutes, &index); if (inactivity_timeout == NULL) { From 355fdbb80ae1105445f6c6b30a2003542870c42a Mon Sep 17 00:00:00 2001 From: NHAS Date: Tue, 14 May 2024 21:49:27 +1200 Subject: [PATCH 05/19] Add device node association --- internal/data/clustering.go | 4 +-- internal/data/devices.go | 29 +++++++++++++++-- internal/data/events.go | 4 +-- internal/router/bpf.go | 8 ++++- internal/router/bpf_bpfeb.go | 3 ++ internal/router/bpf_bpfeb.o | Bin 10184 -> 10824 bytes internal/router/bpf_bpfel.go | 3 ++ internal/router/bpf_bpfel.o | Bin 10256 -> 10928 bytes internal/router/ebpf_test.go | 12 ++++---- internal/router/fwentry.go | 10 ++++-- internal/router/init.go | 53 +++++++++++++------------------- internal/router/statemachine.go | 10 +++--- internal/routetypes/key.go | 2 +- internal/webserver/web.go | 2 +- ui/clustering.go | 4 +-- ui/statemanager.go | 2 +- ui/ui_webserver.go | 5 +-- 17 files changed, 91 insertions(+), 60 deletions(-) diff --git a/internal/data/clustering.go b/internal/data/clustering.go index f476cf3a..170bda6e 100644 --- a/internal/data/clustering.go +++ b/internal/data/clustering.go @@ -33,8 +33,8 @@ type NodeControlRequest struct { Action string } -func GetServerID() string { - return etcdServer.Server.ID().String() +func GetServerID() types.ID { + return etcdServer.Server.ID() } func GetLeader() types.ID { diff --git a/internal/data/devices.go b/internal/data/devices.go index 426b343d..cd159c30 100644 --- a/internal/data/devices.go +++ b/internal/data/devices.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "go.etcd.io/etcd/client/pkg/v3/types" "net" "time" @@ -24,7 +25,7 @@ type Device struct { Active bool Authorised time.Time - AssociatedNode string + AssociatedNode types.ID } func (d Device) String() string { @@ -37,7 +38,10 @@ func (d Device) String() string { return fmt.Sprintf("device[%s:%s:%s][active: %t, attempts: %d, authorised: %s]", d.Username, d.Address, d.AssociatedNode, d.Active, d.Attempts, authorised) } -func UpdateDeviceEndpoint(address string, endpoint *net.UDPAddr) error { +// UpdateDeviceConnectionDetails updates the endpoint we are receiving packets from and the associated cluster node +// I.e if data is coming in to node 3, all other nodes know that the session is only valid while connecting to node 3 +// this stops a race condition where an attacker uses a wireguard profile, but gets load balanced to another node member +func UpdateDeviceConnectionDetails(address string, endpoint *net.UDPAddr) error { realKey, err := etcd.Get(context.Background(), "deviceref-"+address) if err != nil { @@ -191,6 +195,27 @@ func GetAllDevices() (devices []Device, err error) { return devices, nil } +func GetAllDevicesAsMap() (devices map[string]Device, err error) { + + devices = make(map[string]Device) + response, err := etcd.Get(context.Background(), "devices-", clientv3.WithPrefix(), clientv3.WithSort(clientv3.SortByKey, clientv3.SortDescend)) + if err != nil { + return nil, err + } + + for _, res := range response.Kvs { + var device Device + err := json.Unmarshal(res.Value, &device) + if err != nil { + return nil, err + } + + devices[device.Address] = device + } + + return devices, nil +} + func AddDevice(username, publickey string) (Device, error) { preshared_key, err := wgtypes.GenerateKey() diff --git a/internal/data/events.go b/internal/data/events.go index 4d158d89..bb4dbe1d 100644 --- a/internal/data/events.go +++ b/internal/data/events.go @@ -227,7 +227,7 @@ func checkClusterHealth() { func testCluster() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - _, err := etcd.Put(ctx, path.Join(NodeEvents, GetServerID(), "ping"), time.Now().Format(time.RFC1123Z)) + _, err := etcd.Put(ctx, path.Join(NodeEvents, GetServerID().String(), "ping"), time.Now().Format(time.RFC1123Z)) cancel() if err != nil { log.Println("unable to write liveness value") @@ -257,7 +257,7 @@ type EventError struct { func RaiseError(raisedError error, value []byte) (err error) { ee := EventError{ - NodeID: GetServerID(), + NodeID: GetServerID().String(), FailedEventData: string(value), Error: raisedError.Error(), Time: time.Now(), diff --git a/internal/router/bpf.go b/internal/router/bpf.go index e6053428..eccd73b4 100644 --- a/internal/router/bpf.go +++ b/internal/router/bpf.go @@ -102,6 +102,11 @@ func loadXDP() error { return fmt.Errorf("loading objects: %s", err) } + err = xdpObjects.NodeId.Put(uint32(0), uint64(data.GetServerID())) + if err != nil { + return fmt.Errorf("could not set node id: %s", err) + } + sessionInactivityTimeoutMinutes, err := data.GetSessionInactivityTimeoutMinutes() if err != nil { return err @@ -594,7 +599,7 @@ func RefreshUserAcls(username string) error { } // SetAuthroized correctly sets the timestamps for a device with internal IP address as internalAddress -func SetAuthorized(internalAddress, username string) error { +func SetAuthorized(internalAddress, username string, node uint64) error { if net.ParseIP(internalAddress).To4() == nil { return errors.New("internalAddress could not be parsed as an IPv4 address") @@ -605,6 +610,7 @@ func SetAuthorized(internalAddress, username string) error { var deviceStruct fwentry deviceStruct.lastPacketTime = GetTimeStamp() + deviceStruct.associatedNode = node maxSession, err := data.GetSessionLifetimeMinutes() if err != nil { diff --git a/internal/router/bpf_bpfeb.go b/internal/router/bpf_bpfeb.go index 81946d39..e3c37716 100644 --- a/internal/router/bpf_bpfeb.go +++ b/internal/router/bpf_bpfeb.go @@ -63,6 +63,7 @@ type bpfMapSpecs struct { AccountLocked *ebpf.MapSpec `ebpf:"account_locked"` Devices *ebpf.MapSpec `ebpf:"devices"` InactivityTimeoutMinutes *ebpf.MapSpec `ebpf:"inactivity_timeout_minutes"` + NodeId *ebpf.MapSpec `ebpf:"node_Id"` PoliciesTable *ebpf.MapSpec `ebpf:"policies_table"` } @@ -88,6 +89,7 @@ type bpfMaps struct { AccountLocked *ebpf.Map `ebpf:"account_locked"` Devices *ebpf.Map `ebpf:"devices"` InactivityTimeoutMinutes *ebpf.Map `ebpf:"inactivity_timeout_minutes"` + NodeId *ebpf.Map `ebpf:"node_Id"` PoliciesTable *ebpf.Map `ebpf:"policies_table"` } @@ -96,6 +98,7 @@ func (m *bpfMaps) Close() error { m.AccountLocked, m.Devices, m.InactivityTimeoutMinutes, + m.NodeId, m.PoliciesTable, ) } diff --git a/internal/router/bpf_bpfeb.o b/internal/router/bpf_bpfeb.o index d1489286b0fbd0fa743476aa4d1c0f972a8056f2..d8ae494e44e85c833f6a0501074b268381ec31fc 100644 GIT binary patch literal 10824 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 delta 3615 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 delta 3727 zcmZ9Oe@tA*5y$7OZZKTLAurySz8B#H|*5MQKLg*pz7?M6J2 zDj_I2r2Mk%qylZy605Co8ds>T6jHmwjjCc*tx9Mj;WSmO)|E)pMoyDIP?bMWV}*Wa zcYr6a{FwdB%+8yc-F?29zVYtyOki>QM7F>A&&0#qq6HlY5-nre7o4ZvtS@)9Vb;-t zj-Hr!t3B{Ln)v)HRTs1>Oc5<@tYoLg)UVa;R-*Jxt$n0DpykmM>WGzgD@rvTJfcon zO|Gqh`fT-{)l@4Kvrn0o(@U#78Ti>1E$2p5U7)#jLK{bmX0&qj$%|UfnzzubrOh;2 zJ+Ji#v)5MDL&3(v`m2mXn4Zy&&*5(v&xmK%#jH53VvL+MalIZ>{-6}+?KgesN*RBz zw@6P0LquGfx4+mh^4W@6SBFMYr6ISar^MLSV)fpjx$7R|b}s|?i{_% zlyYa()OdY~qeauE1*j~E00*m=P3cEIUnSKTsJ9+epN8rSG3wRfN6wgd2jI`m=(9Lc zwBV;E&RmN|dK|`f*1u+2F&y2j4SstwG{aGowqKvodQ-0G3p!v@8%*8)1)Rcg)b!4; z=bjEnO^=6LP4E47)(l6@9$f*j|Jqp?IP;lq#Ax**WB~>)7h|;gf;P;Vb+oy`=shL! zW#h=3$}i|@Gx}I)pPqQHnA7C_z?^*VhkS&v`3ehIPMDqYUd2vh4B|abBPLx{nknWQ zhmkX!8PT^U6523`xcY@T^;RTN7&hK~C0F4W3z|OEJY({nyIbtYmRH3rbB7Ng3i~&{ zP-dbcJ010q_=B}QFB{~GlY~p3W%ftq?Ye@wW&=^Plskxj)E6+7uXQv`rg9r~@vHg* z@2;|H7IXH01rLvr*UZ`Lsx^FIEuQs1-H7#Tx1U#`b6E-Kna)#}B;xg$4)VR!_ikTM z{XGzG?J+yCaa~Pb8`st3y>VS_^+m8ojXBYx5`FPwfe%;JkDLc`ALxX_^j9sp5PoBr z-q*b6djrRkd++US`g-^MoG7Quuga(vcQAJPQ%m1|wp@jYpCzG#g*YKv?29Uaqiq=@q3|jd&*Wr)>EpZ;#KvJ zirMtBM-Co(bodZ`cVGx7nR}ANn|PGVa}#!JxB6pcs<2<{iJpdhp68bxeUO;nz_WSD zp$sJRJ9rvq!o(jR<}dNopAYMg6>|m!#TLWVDRyQT+%3EpTyix``3){m42u2@1-anE z!h={)eJxB+2%i8~h9WfPvzNnuT=*>PsT(3R?&CCgO8645-yWfvb$$pLLUBP9e2v2G z5n2?k1xND{TJmuTcv+Y)!HxbggQn08+gUQ-NGM% zQ`aJt_i+q-*yo>u|B!GA?q2;(HjN8^2kbVhg{nlM-KKfbz5{K^9X2frFN3S{HeD8u z;%sm0wP{KCW^m7tP0K!xgG-Se*ERvW`K2BIzx70UQTz!Gwc|ED zAzX_?=e}&yapA9mtEN$gum|3H-lj<(-vyrb@c=lFLO22T?*b2PaDhB1rbO`t9Jb6O zfbbT4gEsxarWxU#;2W=?L&DF3vrFiRFc-t)s?WX$_7!1l3Az8`&xJ{-#zy__icLvT zw8EAN;X)x1&iF7mx!s|5;j`e9W`}lPXCD)O7xucmLzBWf8~m9TE_Y!6rb8FjxB$gY zC@!ukz;}aj33URzpcA|#{1gZ1cWBwiS#SoGal-$Gefu#a?Bg8f6_!np%x%tO}hM?Ydzy3d>=VtmxoxGTL)jtBF z=iT&=-)ji!CKIWTiQG5QvfZ8wX5n~|*|+jhx%k*y+IM7Fhxz1c|haQYtW zq %s", device.Address, device.Username, device.Endpoint.String(), p.Endpoint.String()) + } else { + log.Printf("%s:%s roamed associated node %s -> %s", device.Address, device.Username, device.AssociatedNode, data.GetServerID()) } - err = data.UpdateDeviceEndpoint(p.AllowedIPs[0].IP.String(), p.Endpoint) + // TODO this will be updated to be a challenge in the future instead of immediately deauthing + err := data.DeauthenticateDevice(device.Address) if err != nil { - log.Println(ip, "unable to update device endpoint: ", err) + log.Printf("failed to signal cluster of device (%s:%s) deauth: %s", device.Address, device.Username, err) } - //Dont try and remove rules, if we've just started - if !startup { - log.Println(ip, "endpoint changed", d.Endpoint.String(), "->", p.Endpoint.String()) - if err := Deauthenticate(ip); err != nil { - log.Println(ip, "unable to remove forwards for device: ", err) - } + err = data.UpdateDeviceConnectionDetails(p.AllowedIPs[0].IP.String(), p.Endpoint) + if err != nil { + log.Printf("unable to update device (%s:%s) endpoint: %s", device.Address, device.Username, err) } + } } - - startup = false } } diff --git a/internal/router/statemachine.go b/internal/router/statemachine.go index ff63d635..45648157 100644 --- a/internal/router/statemachine.go +++ b/internal/router/statemachine.go @@ -99,8 +99,9 @@ func deviceChanges(key string, current, previous data.Device, et data.EventType) return fmt.Errorf("cannot get lockout: %s", err) } - if (current.Attempts != previous.Attempts && current.Attempts > lockout) || // If the number of authentication attempts on a device has exceeded the max + if current.Attempts > lockout || // If the number of authentication attempts on a device has exceeded the max current.Endpoint.String() != previous.Endpoint.String() || // If the client ip has changed + current.AssociatedNode != previous.AssociatedNode || // If the node the client was sending to is now different current.Authorised.IsZero() { // If we've explicitly deauthorised a device err := Deauthenticate(current.Address) if err != nil { @@ -110,9 +111,10 @@ func deviceChanges(key string, current, previous data.Device, et data.EventType) } - if current.Authorised != previous.Authorised { - if !current.Authorised.IsZero() && current.Attempts <= lockout { - err := SetAuthorized(current.Address, current.Username) + // If the authorisation state has changed and is not disabled + if current.Authorised != previous.Authorised && !current.Authorised.IsZero() { + if current.Attempts <= lockout && current.AssociatedNode == previous.AssociatedNode { + err := SetAuthorized(current.Address, current.Username, uint64(current.AssociatedNode)) if err != nil { return fmt.Errorf("cannot authorize device %s: %s", current.Address, err) } diff --git a/internal/routetypes/key.go b/internal/routetypes/key.go index 881ff1a7..8f432b12 100644 --- a/internal/routetypes/key.go +++ b/internal/routetypes/key.go @@ -10,7 +10,7 @@ import ( type Key struct { // first member must be a prefix u32 wide - // rest can are arbitrary + // rest can be arbitrary Prefixlen uint32 IP [4]byte } diff --git a/internal/webserver/web.go b/internal/webserver/web.go index 939c1af1..daadce0f 100644 --- a/internal/webserver/web.go +++ b/internal/webserver/web.go @@ -384,7 +384,7 @@ func authorise(w http.ResponseWriter, r *http.Request) { func reachability(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", "text/plain") - isDrained, err := data.IsDrained(data.GetServerID()) + isDrained, err := data.IsDrained(data.GetServerID().String()) if err != nil { http.Error(w, "Failed to fetch state", http.StatusInternalServerError) return diff --git a/ui/clustering.go b/ui/clustering.go index 4cfe7314..859d12e7 100644 --- a/ui/clustering.go +++ b/ui/clustering.go @@ -48,7 +48,7 @@ func clusterMembersUI(w http.ResponseWriter, r *http.Request) { }, Leader: data.GetLeader(), - CurrentNode: data.GetServerID(), + CurrentNode: data.GetServerID().String(), } members := data.GetMembers() @@ -184,7 +184,7 @@ func nodeControl(w http.ResponseWriter, r *http.Request) { log.Println("attempting to remove node ", ncR.Node) - if data.GetServerID() == ncR.Node { + if data.GetServerID().String() == ncR.Node { log.Println("user tried to remove current operating node from cluster") http.Error(w, "cannot remove current node", http.StatusBadRequest) return diff --git a/ui/statemanager.go b/ui/statemanager.go index b9e9b00f..1f4f96b8 100644 --- a/ui/statemanager.go +++ b/ui/statemanager.go @@ -9,5 +9,5 @@ var ( func watchClusterHealth(state string) { clusterState = state - serverID = data.GetServerID() + serverID = data.GetServerID().String() } diff --git a/ui/ui_webserver.go b/ui/ui_webserver.go index fda88cdb..5daf9997 100644 --- a/ui/ui_webserver.go +++ b/ui/ui_webserver.go @@ -226,7 +226,7 @@ func StartWebServer(errs chan<- error) error { if data.HasLeader() { clusterState = "healthy" } - serverID = data.GetServerID() + serverID = data.GetServerID().String() _, err = data.RegisterClusterHealthListener(watchClusterHealth) if err != nil { @@ -237,9 +237,6 @@ func StartWebServer(errs chan<- error) error { //https://blog.cloudflare.com/exposing-go-on-the-internet/ tlsConfig := &tls.Config{ - // Causes servers to use Go's default ciphersuite preferences, - // which are tuned to avoid attacks. Does nothing on clients. - PreferServerCipherSuites: true, // Only use curves which have assembly implementations CurvePreferences: []tls.CurveID{ tls.CurveP256, From 6c25f6de17f7cc4a49b1b3e1d29a8b0632ba713c Mon Sep 17 00:00:00 2001 From: NHAS Date: Tue, 14 May 2024 21:50:02 +1200 Subject: [PATCH 06/19] Add object files --- internal/router/bpf_bpfeb.o | Bin 10824 -> 10824 bytes internal/router/bpf_bpfel.o | Bin 10928 -> 10928 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/internal/router/bpf_bpfeb.o b/internal/router/bpf_bpfeb.o index d8ae494e44e85c833f6a0501074b268381ec31fc..4a7b25f59995fdbd7ae010c6a4d1c83403d489f7 100644 GIT binary patch delta 1357 zcmZ9~yK7TX90&04ZIbgyT8(Yh`c8c{)eapLI*2F~%~CAXLBS!4&^TGKLx$h_pvqlrmjk`u0;;z_9KUbg4O&!O%eV!IZ2fbAvmSEwg$d_AyRu;JaIq3Rj7TjKs0 zs?|-b{p+CJjyJ>;(4`(AGU7StJoBTzz|4;d-h3xdbcP?EXvaI~DQ-BD6H3ezi^e4$ ze}|rS374e9*(JykZB8)dsd8 zRsO%Y6ku+`b{=HB;^TZLk77Y_GT~d;yi1s8zgxihI9&^TmR<^6qCa@s1F*erd@C?3 z)_vkR*ur7D7GYWmtK_d zE}sSHM&L0PRta3DtEYTN_BL$gGCO2I_BX7%0@L(yzLUqa`Qe6hIoMCpqiCS;>iWL* F&cE>}vJ3zK delta 1357 zcmZ9~Pe@cj90&04bN9WOx0|)CR#sMPSw73dUV=SD7)b3QX+aML9kvkMAjs&lhv+RK zDAB>_B2nNa@Rk^bL6DHp9t;dTc<8W@P^a+NL89-x`6WXS_PzPc?>B#bGus?(jxOfA z;z97y{5|CFHNx^fek1W3{BQki7sY?zw@$NNvi2hQI|tYF@oYDwj1IJ1W_m3GvZeWN~hSKb==4Hy!b1EiW$G?*etkg-KG$X`)sd^ zdtwArd7|~NgEl+f7LOp<=>R+}o<^|Be6Pzf^Sy#K-^t?*^1~Btdk@vi4SRAziFsmP zMdI;ysK_LYNr$mXkR#fiVA94@?NDR90&iA)3+e_d^yaMjP9AT8A8sJ^2x^=s>@~$} zP&KpRb?Z2_4K;O`9hnHO~#S6~#+XAF{Mmoc;{8 zYVxZ#?q{KzCf^nq!yka!8sJXlNE2(iAOlR?SQhYaL;dcd<;46?P&=G3Rh9f7&^Z>A znzRl&pu0JDs^Rz|J!2dLNy1GaU2+nx(G|z{=(_kNbdmpwbVK|WdSEY|Ovw4ZKo1?I z7i?Uefi9n=mmLq$n{>=OQqQ5UvVwF=5z36BCLDQyuQMl J@nc(i{sJe)v@!qy diff --git a/internal/router/bpf_bpfel.o b/internal/router/bpf_bpfel.o index 7e7f0bc9d549b9149c448f3e465c874ae545853d..75ce5bcbac6984fb42ae6615f7eeab05817c35fa 100644 GIT binary patch delta 1264 zcmY+>O-NKx6bJBsmhaxHy?Q!MI^)NTX*qr@hA40q6;VSW4VW-Ni?K~_*GMj0NY6!} zh>MBmLZZM$feWF*J}_FesD;D?6SQzCB-A3pZJX$S;RLtE@aFeF=bm@od$pUs=MDkMf`@`*Ll?yptovmTCC5p+>^L$E zLJ98jKzi20oOqe;ocFNc)H_&T5zd3>xqJb=3x8A7Hxl^(B;?y%|`qwm5FK!JCfF!`mYok;sZL>P}mi>9|3ym3VY&n^yCkP z1IK-I!0qhAJ!0MbRLG0B=vJ+9LChA!VY=;7pi!2rwV7u+G(zzhJ(kz#;x2ZkwWhQG zJARk$i!N)FB>rH>6gXD=olf1>sE89D&=qPdI6hA=Ii9A+xw5^j?Y}`69na8n;-{>i zNz8Mg1g{w!o8v(7XS%zpu^~RKfRWXBxjPfBYV1n-n5kLQ*b~3E-qZlsw`+W*bK9(o zTUg5OY6P5a>sM)ipKnY2uU-=0WBue&gNm3(5a)W@qQIak*&FtZ(rb<{(i@Ho^tL!- zIJ3iG$MFaa?rL{hYvuRETgi+nJ}P_;(nO&+q&D|K9Jtz1mc5s`jhP zA9JArJml{=e<$)T(&8;%hrfi7760b-bTfpUIDG_&9)ys$?9v7CICHPlLD6!QE?M?n z7Rpk%!vl%5gL&~X-IaAvvGPvlOX6qD2XYRomWSvy@n>_s;GkyNr8mW@1(OKPIM|j# ze+$sD;Gk*QqxZ!V%p(U5GzXhEwrKk64q}$Qx)WTB_f8Kg0C^V$H#j00U*cR{8@dbrlamO(rSy5<;&(RZ4 z6!tCm(rw(%JlrGZ?wUeYyg|3VRTvYq1wlE{ezC4ll3HtsXBvDGafZ(9Da5#onQ5%) zzW*J(N%ZxvlBHK^H8~(evV` z%+E%$94LjCEF8~pp!gHreMw_od`1By`Cz#_<>xharTUmv_qs+?{LZ+f0j_V>*rC&R znTx}W`c^dBINjt|Xm3>`CjOfj#rK$>de7G&<`D$xWV>J2s7mb>dq(M1%NOW%%XxZJ zoN_s{q0z8BLW8@Sofer7?r=xT?R1}bgSkKaT_bB1PSQE?9vc#!F8F004!j)ZfBAq9 zVL3$e%RJ0Y|0w(U<^A(GUEoe;A(NNPJn{W7BDk640V(|84ct<2wI|gw?*?;2{V!H} G2E9Kh)33Jx From 9282a5805cc9ea5ccfcf7ea0756293bea602b36f Mon Sep 17 00:00:00 2001 From: NHAS Date: Tue, 14 May 2024 21:50:46 +1200 Subject: [PATCH 07/19] Fix test add new devices --- internal/router/ebpf_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/router/ebpf_test.go b/internal/router/ebpf_test.go index 10c7a4ba..ecf1b557 100644 --- a/internal/router/ebpf_test.go +++ b/internal/router/ebpf_test.go @@ -63,7 +63,7 @@ func TestBlankPacket(t *testing.T) { func TestAddNewDevices(t *testing.T) { var ipBytes []byte - var deviceBytes = make([]byte, 40) + var deviceBytes = make([]byte, 48) found := map[string]bool{} From 73f648b2e7b2d8c57f9cbafd80d2e91774a1d0b7 Mon Sep 17 00:00:00 2001 From: NHAS Date: Tue, 14 May 2024 21:55:00 +1200 Subject: [PATCH 08/19] Update ebpf tests --- internal/router/ebpf_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/router/ebpf_test.go b/internal/router/ebpf_test.go index ecf1b557..5e49fac3 100644 --- a/internal/router/ebpf_test.go +++ b/internal/router/ebpf_test.go @@ -212,7 +212,7 @@ func TestRoutePriority(t *testing.T) { func TestBasicAuthorise(t *testing.T) { - err := SetAuthorized(devices["tester"].Address, devices["tester"].Username, 0) + err := SetAuthorized(devices["tester"].Address, devices["tester"].Username, uint64(data.GetServerID())) if err != nil { t.Fatal(err) } @@ -426,7 +426,7 @@ func TestRoutePreference(t *testing.T) { func TestSlidingWindow(t *testing.T) { - err := SetAuthorized(devices["tester"].Address, devices["tester"].Username, 0) + err := SetAuthorized(devices["tester"].Address, devices["tester"].Username, uint64(data.GetServerID())) if err != nil { t.Fatal(err) } @@ -529,7 +529,7 @@ func TestSlidingWindow(t *testing.T) { func TestCompositeRules(t *testing.T) { - err := SetAuthorized(devices["tester"].Address, devices["tester"].Username, 0) + err := SetAuthorized(devices["tester"].Address, devices["tester"].Username, uint64(data.GetServerID())) if err != nil { t.Fatal(err) } @@ -632,7 +632,7 @@ func TestDisabledSlidingWindow(t *testing.T) { t.Fatalf("the inactivity timeout was not set to max uint64, was %d (maxuint64 %d)", timeoutFromMap, uint64(math.MaxUint64)) } - err = SetAuthorized(devices["tester"].Address, devices["tester"].Username, 0) + err = SetAuthorized(devices["tester"].Address, devices["tester"].Username, uint64(data.GetServerID())) if err != nil { t.Fatal(err) } @@ -688,7 +688,7 @@ func TestDisabledSlidingWindow(t *testing.T) { func TestMaxSessionLifetime(t *testing.T) { - err := SetAuthorized(devices["tester"].Address, devices["tester"].Username, 0) + err := SetAuthorized(devices["tester"].Address, devices["tester"].Username, uint64(data.GetServerID())) if err != nil { t.Fatal(err) } @@ -753,7 +753,7 @@ func TestDisablingMaxLifetime(t *testing.T) { t.Fatal(err) } - err = SetAuthorized(devices["tester"].Address, devices["tester"].Username, 0) + err = SetAuthorized(devices["tester"].Address, devices["tester"].Username, uint64(data.GetServerID())) if err != nil { t.Fatal(err) } From 97dbcd341f254a883ef7dba86cc9f2a460842cdd Mon Sep 17 00:00:00 2001 From: NHAS Date: Tue, 14 May 2024 22:09:53 +1200 Subject: [PATCH 09/19] Move peer watcher over to tracking reality for that node and monitoring changes there --- internal/router/init.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/internal/router/init.go b/internal/router/init.go index 87cc8952..816d555b 100644 --- a/internal/router/init.go +++ b/internal/router/init.go @@ -54,6 +54,7 @@ func Setup(errorChan chan<- error, iptables bool) (err error) { go func() { + ourPeerAddresses := make(map[string]string) for { select { @@ -84,20 +85,22 @@ func Setup(errorChan chan<- error, iptables bool) (err error) { continue } - if device.Endpoint.String() != p.Endpoint.String() || device.AssociatedNode != data.GetServerID() { + if _, ok := ourPeerAddresses[device.Address]; !ok { + ourPeerAddresses[device.Address] = p.Endpoint.String() + } - if device.Endpoint.String() != p.Endpoint.String() { - log.Printf("%s:%s endpoint changed %s -> %s", device.Address, device.Username, device.Endpoint.String(), p.Endpoint.String()) - } else { - log.Printf("%s:%s roamed associated node %s -> %s", device.Address, device.Username, device.AssociatedNode, data.GetServerID()) - } + if ourPeerAddresses[device.Address] != p.Endpoint.String() { + + ourPeerAddresses[device.Address] = p.Endpoint.String() + + log.Printf("%s:%s endpoint changed %s -> %s", device.Address, device.Username, device.Endpoint.String(), p.Endpoint.String()) - // TODO this will be updated to be a challenge in the future instead of immediately deauthing - err := data.DeauthenticateDevice(device.Address) + err = data.DeauthenticateDevice(device.Address) if err != nil { - log.Printf("failed to signal cluster of device (%s:%s) deauth: %s", device.Address, device.Username, err) + log.Printf("failed to deauth device (%s:%s) endpoint: %s", device.Address, device.Username, err) } + // This will set the association to this node automatically err = data.UpdateDeviceConnectionDetails(p.AllowedIPs[0].IP.String(), p.Endpoint) if err != nil { log.Printf("unable to update device (%s:%s) endpoint: %s", device.Address, device.Username, err) From 2840f0abd550e905f0f1268c1be0d22b2ec6c0dc Mon Sep 17 00:00:00 2001 From: NHAS Date: Tue, 14 May 2024 22:16:23 +1200 Subject: [PATCH 10/19] Move ebpf node association check into mfa routes --- internal/router/ebpf_test.go | 1 - internal/router/xdp.c | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/router/ebpf_test.go b/internal/router/ebpf_test.go index 5e49fac3..1b186da1 100644 --- a/internal/router/ebpf_test.go +++ b/internal/router/ebpf_test.go @@ -956,7 +956,6 @@ func TestAgnosticRuleOrdering(t *testing.T) { for _, user := range devices { acl := data.GetEffectiveAcl(user.Username) - log.Println(user, acl.Allow) rules, err := routetypes.ParseRules(nil, acl.Allow, nil) if err != nil { t.Fatal(err) diff --git a/internal/router/xdp.c b/internal/router/xdp.c index 06800019..b3509344 100644 --- a/internal/router/xdp.c +++ b/internal/router/xdp.c @@ -521,7 +521,7 @@ static __always_inline int conntrack(struct ip *ip_info) // If the traffic comes from a peer that we are not associated with, i.e traffic is coming to a node who has not talked to this peer before // kill it - if(*current_node_id != current_device->associatedNode) { + if() { return 0; } @@ -599,7 +599,9 @@ static __always_inline int conntrack(struct ip *ip_info) // If device does not belong to a locked account, the device itself isnt locked and if it isnt timed out return (!*isAccountLocked && !isTimedOut && current_device->sessionExpiry != 0 && // If either max session lifetime is disabled, or it is before the max lifetime of the session - (current_device->sessionExpiry == __UINT64_MAX__ || currentTime < current_device->sessionExpiry)); + (current_device->sessionExpiry == __UINT64_MAX__ || currentTime < current_device->sessionExpiry) && + // If the device is associated with this node + *current_node_id == current_device->associatedNode ; } } } From 8c360fddb579a821e7fbffe31cc25f0c414ab6b2 Mon Sep 17 00:00:00 2001 From: NHAS Date: Tue, 14 May 2024 22:17:56 +1200 Subject: [PATCH 11/19] Revert that change --- internal/router/xdp.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/internal/router/xdp.c b/internal/router/xdp.c index b3509344..06800019 100644 --- a/internal/router/xdp.c +++ b/internal/router/xdp.c @@ -521,7 +521,7 @@ static __always_inline int conntrack(struct ip *ip_info) // If the traffic comes from a peer that we are not associated with, i.e traffic is coming to a node who has not talked to this peer before // kill it - if() { + if(*current_node_id != current_device->associatedNode) { return 0; } @@ -599,9 +599,7 @@ static __always_inline int conntrack(struct ip *ip_info) // If device does not belong to a locked account, the device itself isnt locked and if it isnt timed out return (!*isAccountLocked && !isTimedOut && current_device->sessionExpiry != 0 && // If either max session lifetime is disabled, or it is before the max lifetime of the session - (current_device->sessionExpiry == __UINT64_MAX__ || currentTime < current_device->sessionExpiry) && - // If the device is associated with this node - *current_node_id == current_device->associatedNode ; + (current_device->sessionExpiry == __UINT64_MAX__ || currentTime < current_device->sessionExpiry)); } } } From 442d33020056ee9f152aa404ecde537d35859303 Mon Sep 17 00:00:00 2001 From: NHAS Date: Tue, 14 May 2024 22:20:00 +1200 Subject: [PATCH 12/19] Add node association in epbf etests --- internal/router/bpf.go | 3 ++- internal/router/ebpf_test.go | 2 +- internal/router/wireguard.go | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/router/bpf.go b/internal/router/bpf.go index eccd73b4..a7212689 100644 --- a/internal/router/bpf.go +++ b/internal/router/bpf.go @@ -266,7 +266,7 @@ func xdpRemoveDevice(address string) error { return finalError } -func xdpAddDevice(username, address string) error { +func xdpAddDevice(username, address string, associatedNode uint64) error { ip := net.ParseIP(address) if ip == nil { @@ -284,6 +284,7 @@ func xdpAddDevice(username, address string) error { deviceStruct.lastPacketTime = 0 deviceStruct.sessionExpiry = 0 deviceStruct.user_id = sha1.Sum([]byte(username)) + deviceStruct.associatedNode = associatedNode if err := xdpUserExists(deviceStruct.user_id); err != nil { return err diff --git a/internal/router/ebpf_test.go b/internal/router/ebpf_test.go index 1b186da1..3274bbf3 100644 --- a/internal/router/ebpf_test.go +++ b/internal/router/ebpf_test.go @@ -1158,7 +1158,7 @@ func addDevices() error { return err } - err = xdpAddDevice(device.Username, device.Address) + err = xdpAddDevice(device.Username, device.Address, uint64(data.GetServerID())) if err != nil { return err } diff --git a/internal/router/wireguard.go b/internal/router/wireguard.go index 4e58da70..dbf387ff 100644 --- a/internal/router/wireguard.go +++ b/internal/router/wireguard.go @@ -275,7 +275,7 @@ func AddPeer(public wgtypes.Key, username, addresss, presharedKey string) (err e }, } - err = xdpAddDevice(username, addresss) + err = xdpAddDevice(username, addresss, 0) if err != nil { return err From 62a376516d0bef80656e08e6137ca5449390403e Mon Sep 17 00:00:00 2001 From: NHAS Date: Tue, 14 May 2024 22:26:00 +1200 Subject: [PATCH 13/19] Fix syntax --- internal/router/bpf.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/router/bpf.go b/internal/router/bpf.go index a7212689..8f35ff65 100644 --- a/internal/router/bpf.go +++ b/internal/router/bpf.go @@ -169,7 +169,7 @@ func setupXDP(users []data.UserModel, knownDevices []data.Device) error { for _, device := range knownDevices { - err := xdpAddDevice(device.Username, device.Address) + err := xdpAddDevice(device.Username, device.Address, 0) if err != nil { return errors.New("xdp setup add device to user: " + err.Error()) } From 16c65e569c94c5081536b563a1a9e171339eb492 Mon Sep 17 00:00:00 2001 From: NHAS Date: Wed, 15 May 2024 09:56:31 +1200 Subject: [PATCH 14/19] Fix linter issues --- commands/cleanup.go | 12 ++-- commands/start.go | 1 - internal/config/config.go | 2 +- internal/data/clustering.go | 6 +- internal/data/config.go | 63 ------------------- internal/data/devices.go | 6 +- internal/data/events.go | 19 ------ internal/data/init.go | 2 +- internal/router/bpf.go | 3 +- internal/router/iptables.go | 2 +- internal/router/statemachine.go | 31 ++++----- internal/router/wireguard.go | 16 ----- .../authenticators/authenticators.go | 18 +++--- internal/webserver/authenticators/pam.go | 8 ++- internal/webserver/authenticators/totp.go | 8 ++- internal/webserver/authenticators/webauthn.go | 16 ++--- internal/webserver/statemachine.go | 8 +-- internal/webserver/web.go | 27 ++++---- main.go | 3 +- pkg/control/server/server.go | 2 +- pkg/control/server/users.go | 14 +++-- pkg/control/wagctl/client.go | 26 ++++---- ui/devices.go | 6 +- ui/diagnostics.go | 6 +- ui/groups.go | 3 + ui/notifications.go | 2 +- ui/policies.go | 3 + ui/registration.go | 8 ++- ui/security.go | 2 +- ui/ui_webserver.go | 7 ++- ui/users.go | 5 +- 31 files changed, 127 insertions(+), 208 deletions(-) diff --git a/commands/cleanup.go b/commands/cleanup.go index 81c02b83..70541830 100644 --- a/commands/cleanup.go +++ b/commands/cleanup.go @@ -3,13 +3,12 @@ package commands import ( "flag" "fmt" - "log" - "os" - "os/exec" - "github.com/NHAS/wag/internal/config" + "github.com/NHAS/wag/internal/data" "github.com/NHAS/wag/internal/router" "github.com/NHAS/wag/pkg/control/server" + "log" + "os" ) type cleanup struct { @@ -61,11 +60,12 @@ func (g *cleanup) Run() error { if result != "0" && result != "3" { log.Println("Cleaning up") + router.TearDown(true) server.TearDown() - exec.Command("/usr/bin/wg-quick", "save", config.Values.Wireguard.DevName).Run() + data.TearDown() - return exec.Command("/usr/bin/wg-quick", "down", config.Values.Wireguard.DevName).Run() + return nil } diff --git a/commands/start.go b/commands/start.go index 8ba72127..65949ebf 100644 --- a/commands/start.go +++ b/commands/start.go @@ -105,7 +105,6 @@ func teardown(force bool) { ui.Teardown() webserver.Teardown() - } func clusterState(noIptables bool, errorChan chan<- error) func(string) { diff --git a/internal/config/config.go b/internal/config/config.go index a7d7bafb..7073e870 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -390,7 +390,7 @@ func parseAddress(address string) ([]string, error) { return nil, fmt.Errorf("no addresses for %s", address) } - output := []string{} + var output []string addedSomething := false for _, addr := range addresses { if addr.To4() != nil { diff --git a/internal/data/clustering.go b/internal/data/clustering.go index 170bda6e..8272e995 100644 --- a/internal/data/clustering.go +++ b/internal/data/clustering.go @@ -49,11 +49,7 @@ func IsLearner() bool { return etcdServer.Server.IsLearner() } -func IsLeader() bool { - return etcdServer.Server.Leader() == etcdServer.Server.ID() -} - -// Called on a leader node, to transfer ownership to another node (demoted) +// StepDown when called on a leader node, to transfer ownership to another node (demoted) func StepDown() error { return etcdServer.Server.TransferLeadership() } diff --git a/internal/data/config.go b/internal/data/config.go index c8bd6276..27a3b4e5 100644 --- a/internal/data/config.go +++ b/internal/data/config.go @@ -8,7 +8,6 @@ import ( "net/url" "strings" - "github.com/NHAS/wag/internal/data/validators" "github.com/go-playground/validator/v10" clientv3 "go.etcd.io/etcd/client/v3" ) @@ -92,16 +91,6 @@ func getInt(key string) (ret int, err error) { return ret, nil } -func SetPAM(details PAM) error { - d, err := json.Marshal(details) - if err != nil { - return err - } - - _, err = etcd.Put(context.Background(), PamDetailsKey, string(d)) - return err -} - func GetPAM() (details PAM, err error) { response, err := etcd.Get(context.Background(), OidcDetailsKey) @@ -117,16 +106,6 @@ func GetPAM() (details PAM, err error) { return } -func SetOidc(details OIDC) error { - d, err := json.Marshal(details) - if err != nil { - return err - } - - _, err = etcd.Put(context.Background(), OidcDetailsKey, string(d)) - return err -} - func GetOidc() (details OIDC, err error) { response, err := etcd.Get(context.Background(), OidcDetailsKey) @@ -175,13 +154,6 @@ func GetWebauthn() (wba Webauthn, err error) { return } -func SetWireguardConfigName(wgConfig string) error { - data, _ := json.Marshal(wgConfig) - - _, err := etcd.Put(context.Background(), defaultWGFileNameKey, string(data)) - return err -} - func GetWireguardConfigName() string { k, err := getString(defaultWGFileNameKey) if err != nil { @@ -232,14 +204,6 @@ func GetAuthenicationMethods() (result []string, err error) { return } -func SetCheckUpdates(doChecks bool) error { - - data, _ := json.Marshal(doChecks) - - _, err := etcd.Put(context.Background(), checkUpdatesKey, string(data)) - return err -} - func ShouldCheckUpdates() (bool, error) { resp, err := etcd.Get(context.Background(), checkUpdatesKey) @@ -261,12 +225,6 @@ func ShouldCheckUpdates() (bool, error) { return ret, nil } -func SetDomain(domain string) error { - data, _ := json.Marshal(domain) - _, err := etcd.Put(context.Background(), DomainKey, string(data)) - return err -} - func GetDomain() (string, error) { return getString(DomainKey) } @@ -297,17 +255,6 @@ func GetHelpMail() string { return mail } -func SetExternalAddress(externalAddress string) error { - - if err := validators.ValidExternalAddresses(externalAddress); err != nil { - return err - } - - data, _ := json.Marshal(externalAddress) - _, err := etcd.Put(context.Background(), externalAddressKey, string(data)) - return err -} - func GetExternalAddress() (string, error) { return getString(externalAddressKey) } @@ -625,16 +572,6 @@ func GetSessionInactivityTimeoutMinutes() (int, error) { return inactivityTimeout, nil } -func SetLockout(accountLockout int) error { - if accountLockout < 1 { - return errors.New("cannot set lockout to be below 1 as all accounts would be locked out") - } - - data, _ := json.Marshal(accountLockout) - _, err := etcd.Put(context.Background(), LockoutKey, string(data)) - return err -} - // Get account lockout threshold setting func GetLockout() (int, error) { lockout, err := getInt(LockoutKey) diff --git a/internal/data/devices.go b/internal/data/devices.go index cd159c30..c585d188 100644 --- a/internal/data/devices.go +++ b/internal/data/devices.go @@ -321,7 +321,7 @@ func DeleteDevices(username string) error { return err } - ops := []clientv3.Op{} + var ops []clientv3.Op for _, reference := range deleted.PrevKvs { var d Device @@ -339,7 +339,7 @@ func DeleteDevices(username string) error { func UpdateDevicePublicKey(username, address string, publicKey wgtypes.Key) error { - beforeUpadte, err := GetDeviceByAddress(address) + beforeUpdate, err := GetDeviceByAddress(address) if err != nil { return err } @@ -366,7 +366,7 @@ func UpdateDevicePublicKey(username, address string, publicKey wgtypes.Key) erro return err } - _, err = etcd.Delete(context.Background(), "devicesref-"+beforeUpadte.Publickey) + _, err = etcd.Delete(context.Background(), "devicesref-"+beforeUpdate.Publickey) return err } diff --git a/internal/data/events.go b/internal/data/events.go index bb4dbe1d..37338ef8 100644 --- a/internal/data/events.go +++ b/internal/data/events.go @@ -147,18 +147,6 @@ func RegisterEventListener[T any](path string, isPrefix bool, f func(key string, return key, nil } -func DeregisterEventListener(key string) { - lck.Lock() - defer lck.Unlock() - - if cancel, ok := contextMaps[key]; ok { - if cancel != nil { - cancel() - } - delete(contextMaps, key) - } -} - func RegisterClusterHealthListener(f func(status string)) (string, error) { clusterHealthLck.Lock() defer clusterHealthLck.Unlock() @@ -173,13 +161,6 @@ func RegisterClusterHealthListener(f func(status string)) (string, error) { return key, nil } -func DeregisterClusterHealthListener(key string) { - clusterHealthLck.Lock() - defer clusterHealthLck.Unlock() - - delete(clusterHealthListeners, key) -} - func notifyClusterHealthListeners(event string) { clusterHealthLck.RLock() defer clusterHealthLck.RUnlock() diff --git a/internal/data/init.go b/internal/data/init.go index bd41c392..69289b28 100644 --- a/internal/data/init.go +++ b/internal/data/init.go @@ -453,7 +453,7 @@ func TearDown() { func doSafeUpdate(ctx context.Context, key string, create bool, mutateFunc func(*clientv3.GetResponse) (value string, err error)) error { //https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go#L382 - opts := []clientv3.OpOption{} + var opts []clientv3.OpOption if mutateFunc == nil { return errors.New("no mutate function set in safe update") diff --git a/internal/router/bpf.go b/internal/router/bpf.go index 8f35ff65..9ee8277a 100644 --- a/internal/router/bpf.go +++ b/internal/router/bpf.go @@ -28,7 +28,6 @@ import ( //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS bpf xdp.c -- -I headers const ( - ebpfFS = "/sys/fs/bpf" CLOCK_BOOTTIME = uint32(7) ) @@ -423,7 +422,7 @@ func clearPolicyMap(toClear *ebpf.Map) error { } err = toClear.Delete(lastKey) - if err != nil && err != ebpf.ErrKeyNotExist { + if err != nil && errors.Is(err, ebpf.ErrKeyNotExist) { return err } diff --git a/internal/router/iptables.go b/internal/router/iptables.go index 4fe7d2ed..53374bce 100644 --- a/internal/router/iptables.go +++ b/internal/router/iptables.go @@ -18,7 +18,7 @@ func setupIptables() error { devName := config.Values.Wireguard.DevName //So. This to the average person will look like we say "Hey server forward anything and everything from the wireguard interface" - //And without the xdp ebpf program it would be, however if you look at xdp.c you can see that we can manipluate maps of addresses for each user + //And without the xdp ebpf program it would be, however if you look at xdp.c you can see that we can manipulate maps of addresses for each user //This then controls whether the packet is dropped, but we still need iptables to do the higher level routing stuffs err = ipt.ChangePolicy("filter", "FORWARD", "DROP") diff --git a/internal/router/statemachine.go b/internal/router/statemachine.go index 45648157..3a3131c9 100644 --- a/internal/router/statemachine.go +++ b/internal/router/statemachine.go @@ -11,47 +11,47 @@ import ( "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) -func handleEvents(erroChan chan<- error) { +func handleEvents(errorChan chan<- error) { _, err := data.RegisterEventListener(data.DevicesPrefix, true, deviceChanges) if err != nil { - erroChan <- err + errorChan <- err return } _, err = data.RegisterEventListener(data.GroupMembershipPrefix, true, membershipChanges) if err != nil { - erroChan <- err + errorChan <- err return } _, err = data.RegisterEventListener(data.UsersPrefix, true, userChanges) if err != nil { - erroChan <- err + errorChan <- err return } _, err = data.RegisterEventListener(data.AclsPrefix, true, aclsChanges) if err != nil { - erroChan <- err + errorChan <- err return } _, err = data.RegisterEventListener(data.GroupsPrefix, true, groupChanges) if err != nil { - erroChan <- err + errorChan <- err return } _, err = data.RegisterEventListener(data.InactivityTimeoutKey, true, inactivityTimeoutChanges) if err != nil { - erroChan <- err + errorChan <- err return } } -func inactivityTimeoutChanges(key string, current, previous int, et data.EventType) error { +func inactivityTimeoutChanges(_ string, current, _ int, et data.EventType) error { switch et { case data.MODIFIED, data.CREATED: @@ -64,7 +64,7 @@ func inactivityTimeoutChanges(key string, current, previous int, et data.EventTy return nil } -func deviceChanges(key string, current, previous data.Device, et data.EventType) error { +func deviceChanges(_ string, current, previous data.Device, et data.EventType) error { switch et { case data.DELETED: @@ -129,7 +129,7 @@ func deviceChanges(key string, current, previous data.Device, et data.EventType) return nil } -func membershipChanges(key string, current, previous []string, et data.EventType) error { +func membershipChanges(key string, _, _ []string, et data.EventType) error { username := strings.TrimPrefix(key, data.GroupMembershipPrefix) switch et { @@ -144,11 +144,11 @@ func membershipChanges(key string, current, previous []string, et data.EventType return nil } -func userChanges(key string, current, previous data.UserModel, et data.EventType) error { +func userChanges(_ string, current, previous data.UserModel, et data.EventType) error { switch et { case data.CREATED: - acls := data.GetEffectiveAcl(current.Username) - err := AddUser(current.Username, acls) + newUserAcls := data.GetEffectiveAcl(current.Username) + err := AddUser(current.Username, newUserAcls) if err != nil { log.Printf("cannot create user %s: %s", current.Username, err) return fmt.Errorf("cannot create user %s: %s", current.Username, err) @@ -189,7 +189,8 @@ func userChanges(key string, current, previous data.UserModel, et data.EventType return nil } -func aclsChanges(key string, current, previous acls.Acl, et data.EventType) error { +func aclsChanges(_ string, _, _ acls.Acl, et data.EventType) error { + // TODO refresh the users that the acl applies to as a potential performance improvement switch et { case data.CREATED, data.DELETED, data.MODIFIED: err := RefreshConfiguration() @@ -202,7 +203,7 @@ func aclsChanges(key string, current, previous acls.Acl, et data.EventType) erro return nil } -func groupChanges(key string, current, previous []string, et data.EventType) error { +func groupChanges(_ string, current, _ []string, et data.EventType) error { switch et { case data.CREATED, data.DELETED, data.MODIFIED: diff --git a/internal/router/wireguard.go b/internal/router/wireguard.go index dbf387ff..74783f5a 100644 --- a/internal/router/wireguard.go +++ b/internal/router/wireguard.go @@ -277,7 +277,6 @@ func AddPeer(public wgtypes.Key, username, addresss, presharedKey string) (err e err = xdpAddDevice(username, addresss, 0) if err != nil { - return err } @@ -298,21 +297,6 @@ func AddPeer(public wgtypes.Key, username, addresss, presharedKey string) (err e return nil } -func GetPeerRealIp(address string) (string, error) { - dev, err := ctrl.Device(config.Values.Wireguard.DevName) - if err != nil { - return "", err - } - - for _, peer := range dev.Peers { - if len(peer.AllowedIPs) == 1 && peer.AllowedIPs[0].IP.String() == address { - return peer.Endpoint.String(), nil - } - } - - return "", errors.New("not found") -} - func addWg(c *netlink.Conn, name string, address net.IPNet, mtu int) error { infomsg := IfInfomsg{ diff --git a/internal/webserver/authenticators/authenticators.go b/internal/webserver/authenticators/authenticators.go index 260d44d7..522a31b3 100644 --- a/internal/webserver/authenticators/authenticators.go +++ b/internal/webserver/authenticators/authenticators.go @@ -58,7 +58,7 @@ func ReinitaliseMethods(method ...types.MFA) ([]types.MFA, error) { lck.Lock() defer lck.Unlock() - out := []types.MFA{} + var out []types.MFA var errRet error for _, m := range method { @@ -95,7 +95,7 @@ func GetAllEnabledMethods() (r []Authenticator) { lck.RLock() defer lck.RUnlock() - order := []string{} + var order []string for k := range allMfa { order = append(order, string(k)) } @@ -115,7 +115,7 @@ func GetAllAvaliableMethods() (r []Authenticator) { lck.RLock() defer lck.RUnlock() - order := []string{} + var order []string for k := range allMfa { order = append(order, string(k)) } @@ -175,22 +175,22 @@ type Authenticator interface { Type() string - // Name that is displayed in the MFA selection table + //FriendlyName is the name that is displayed in the MFA selection table FriendlyName() string - // Redirection path that deauthenticates selected mfa method (mostly just "/" unless its externally connected to something) + //LogoutPath returns the redirection path that deauthenticates selected mfa method (mostly just "/" unless it's externally connected to something) LogoutPath() string - // Automatically added under /register_mfa/ + //RegistrationAPI automatically added under /register_mfa/ RegistrationAPI(w http.ResponseWriter, r *http.Request) - // Automatically added under /authorise/ + //AuthorisationAPI automatically added under /authorise/ AuthorisationAPI(w http.ResponseWriter, r *http.Request) - // Executed in /authorise/ path to display UI when user browses to that path + //MFAPromptUI is executed in /authorise/ path to display UI when user browses to that path MFAPromptUI(w http.ResponseWriter, r *http.Request, username, ip string) - // Executed in /register_mfa/ path to show the UI for registration + //RegistrationUI is executed in /register_mfa/ path to show the UI for registration RegistrationUI(w http.ResponseWriter, r *http.Request, username, ip string) } diff --git a/internal/webserver/authenticators/pam.go b/internal/webserver/authenticators/pam.go index f6e7cceb..03e8933d 100644 --- a/internal/webserver/authenticators/pam.go +++ b/internal/webserver/authenticators/pam.go @@ -77,7 +77,9 @@ func (t *Pam) RegistrationAPI(w http.ResponseWriter, r *http.Request) { } log.Println(user.Username, clientTunnelIp, "authorised") - user.EnforceMFA() + if err := user.EnforceMFA(); err != nil { + log.Println(user.Username, clientTunnelIp, "failed to enforce mfa: ", err) + } default: http.NotFound(w, r) @@ -182,7 +184,7 @@ func (t *Pam) AuthoriseFunc(w http.ResponseWriter, r *http.Request) types.Authen } } -func (t *Pam) MFAPromptUI(w http.ResponseWriter, r *http.Request, username, ip string) { +func (t *Pam) MFAPromptUI(w http.ResponseWriter, _ *http.Request, username, ip string) { if err := resources.Render("prompt_mfa_pam.html", w, &resources.Msg{ HelpMail: data.GetHelpMail(), NumMethods: NumberOfMethods(), @@ -191,7 +193,7 @@ func (t *Pam) MFAPromptUI(w http.ResponseWriter, r *http.Request, username, ip s } } -func (t *Pam) RegistrationUI(w http.ResponseWriter, r *http.Request, username, ip string) { +func (t *Pam) RegistrationUI(w http.ResponseWriter, _ *http.Request, username, ip string) { if err := resources.Render("register_mfa_pam.html", w, &resources.Msg{ HelpMail: data.GetHelpMail(), NumMethods: NumberOfMethods(), diff --git a/internal/webserver/authenticators/totp.go b/internal/webserver/authenticators/totp.go index b8365c21..79a632f1 100644 --- a/internal/webserver/authenticators/totp.go +++ b/internal/webserver/authenticators/totp.go @@ -135,7 +135,9 @@ func (t *Totp) RegistrationAPI(w http.ResponseWriter, r *http.Request) { } log.Println(user.Username, clientTunnelIp, "authorised") - user.EnforceMFA() + if err := user.EnforceMFA(); err != nil { + log.Println(user.Username, clientTunnelIp, "enforce mfa failed:", err) + } default: http.NotFound(w, r) @@ -216,7 +218,7 @@ func (t *Totp) AuthoriseFunc(w http.ResponseWriter, r *http.Request) types.Authe } } -func (t *Totp) MFAPromptUI(w http.ResponseWriter, r *http.Request, username, ip string) { +func (t *Totp) MFAPromptUI(w http.ResponseWriter, _ *http.Request, username, ip string) { if err := resources.Render("prompt_mfa_totp.html", w, &resources.Msg{ HelpMail: data.GetHelpMail(), @@ -226,7 +228,7 @@ func (t *Totp) MFAPromptUI(w http.ResponseWriter, r *http.Request, username, ip } } -func (t *Totp) RegistrationUI(w http.ResponseWriter, r *http.Request, username, ip string) { +func (t *Totp) RegistrationUI(w http.ResponseWriter, _ *http.Request, username, ip string) { if err := resources.Render("register_mfa_totp.html", w, &resources.Msg{ HelpMail: data.GetHelpMail(), diff --git a/internal/webserver/authenticators/webauthn.go b/internal/webserver/authenticators/webauthn.go index dd9aa80a..59baabb7 100644 --- a/internal/webserver/authenticators/webauthn.go +++ b/internal/webserver/authenticators/webauthn.go @@ -35,9 +35,9 @@ func (wa *Webauthn) Init() error { } wa.webauthnExecutor, err = webauthn.New(&webauthn.Config{ - RPDisplayName: d.DisplayName, // Display Name for your site - RPID: d.ID, // Generally the domain name for your site - RPOrigin: d.Origin, // The origin URL for WebAuthn requests + RPDisplayName: d.DisplayName, // Display Name for your site + RPID: d.ID, // Generally the domain name for your site + RPOrigins: []string{d.Origin}, // The origin URL for WebAuthn requests }) if err != nil { return err @@ -288,7 +288,7 @@ func (wa *Webauthn) AuthorisationAPI(w http.ResponseWriter, r *http.Request) { } } -func (wa *Webauthn) MFAPromptUI(w http.ResponseWriter, r *http.Request, username, ip string) { +func (wa *Webauthn) MFAPromptUI(w http.ResponseWriter, _ *http.Request, username, ip string) { if err := resources.Render("prompt_mfa_webauthn.html", w, &resources.Msg{ HelpMail: data.GetHelpMail(), @@ -298,7 +298,7 @@ func (wa *Webauthn) MFAPromptUI(w http.ResponseWriter, r *http.Request, username } } -func (wa *Webauthn) RegistrationUI(w http.ResponseWriter, r *http.Request, username, ip string) { +func (wa *Webauthn) RegistrationUI(w http.ResponseWriter, _ *http.Request, username, ip string) { if err := resources.Render("register_mfa_webauthn.html", w, &resources.Msg{ HelpMail: data.GetHelpMail(), @@ -392,7 +392,7 @@ func randomUint64() uint64 { // WebAuthnID returns the user's ID func (u WebauthnUser) WebAuthnID() []byte { buf := make([]byte, binary.MaxVarintLen64) - binary.PutUvarint(buf, uint64(u.id)) + binary.PutUvarint(buf, u.id) return buf } @@ -418,7 +418,7 @@ func (u *WebauthnUser) AddCredential(cred webauthn.Credential) { } -// WebAuthnCredentials returns credentials owned by the user +// WebAuthnCredential returns credential owned by the user func (u WebauthnUser) WebAuthnCredential(ID []byte) (out *webauthn.Credential) { return u.credentials[string(ID)] @@ -437,7 +437,7 @@ func (u WebauthnUser) WebAuthnCredentials() (out []*webauthn.Credential) { // with all the user's credentials func (u WebauthnUser) CredentialExcludeList() []protocol.CredentialDescriptor { - credentialExcludeList := []protocol.CredentialDescriptor{} + var credentialExcludeList []protocol.CredentialDescriptor for _, cred := range u.credentials { descriptor := protocol.CredentialDescriptor{ Type: protocol.PublicKeyCredentialType, diff --git a/internal/webserver/statemachine.go b/internal/webserver/statemachine.go index df88c313..9b7a3677 100644 --- a/internal/webserver/statemachine.go +++ b/internal/webserver/statemachine.go @@ -33,7 +33,7 @@ func registerListeners() error { } // OidcDetailsKey = "wag-config-authentication-oidc" -func oidcChanges(key string, current data.OIDC, previous data.OIDC, et data.EventType) error { +func oidcChanges(_ string, _ data.OIDC, _ data.OIDC, et data.EventType) error { switch et { case data.DELETED: authenticators.DisableMethods(types.Oidc) @@ -56,7 +56,7 @@ func oidcChanges(key string, current data.OIDC, previous data.OIDC, et data.Even } // DomainKey = "wag-config-authentication-domain" -func domainChanged(key string, current string, _ string, et data.EventType) error { +func domainChanged(_ string, _ string, _ string, et data.EventType) error { switch et { case data.MODIFIED: @@ -77,7 +77,7 @@ func domainChanged(key string, current string, _ string, et data.EventType) erro } // MethodsEnabledKey = "wag-config-authentication-methods" -func enabledMethodsChanged(key string, current []string, previous []string, et data.EventType) (err error) { +func enabledMethodsChanged(_ string, current, previous []string, et data.EventType) (err error) { switch et { case data.DELETED: authenticators.DisableMethods(authenticators.StringsToMFA(previous)...) @@ -100,7 +100,7 @@ func enabledMethodsChanged(key string, current []string, previous []string, et d } // IssuerKey = "wag-config-authentication-issuer" -func issuerKeyChanged(key string, current string, previous string, et data.EventType) error { +func issuerKeyChanged(_ string, _, _ string, et data.EventType) error { switch et { case data.DELETED: authenticators.DisableMethods(types.Totp, types.Webauthn) diff --git a/internal/webserver/web.go b/internal/webserver/web.go index daadce0f..f868dfba 100644 --- a/internal/webserver/web.go +++ b/internal/webserver/web.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "encoding/base64" "encoding/json" + "errors" "fmt" "html/template" "image/png" @@ -96,7 +97,7 @@ func Start(errChan chan<- error) error { Handler: setSecurityHeaders(public), } - if err := publicTLSServ.ListenAndServeTLS(config.Values.Webserver.Public.CertPath, config.Values.Webserver.Public.KeyPath); err != nil && err != http.ErrServerClosed { + if err := publicTLSServ.ListenAndServeTLS(config.Values.Webserver.Public.CertPath, config.Values.Webserver.Public.KeyPath); err != nil && !errors.Is(err, http.ErrServerClosed) { errChan <- fmt.Errorf("TLS webserver public listener failed: %v", err) } }() @@ -139,7 +140,7 @@ func Start(errChan chan<- error) error { Handler: setSecurityHeaders(public), } - if err := publicHTTPServ.ListenAndServe(); err != nil && err != http.ErrServerClosed { + if err := publicHTTPServ.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { errChan <- fmt.Errorf("HTTP webserver public listener failed: %v", err) } }() @@ -191,7 +192,7 @@ func Start(errChan chan<- error) error { TLSConfig: tlsConfig, Handler: setSecurityHeaders(tunnel), } - if err := tunnelTLSServ.ListenAndServeTLS(config.Values.Webserver.Tunnel.CertPath, config.Values.Webserver.Tunnel.KeyPath); err != nil && err != http.ErrServerClosed { + if err := tunnelTLSServ.ListenAndServeTLS(config.Values.Webserver.Tunnel.CertPath, config.Values.Webserver.Tunnel.KeyPath); err != nil && errors.Is(err, http.ErrServerClosed) { errChan <- fmt.Errorf("TLS webserver tunnel listener failed: %v", err) } @@ -226,14 +227,14 @@ func Start(errChan chan<- error) error { Handler: setSecurityHeaders(tunnel), } - if err := tunnelHTTPServ.ListenAndServe(); err != nil && err != http.ErrServerClosed { + if err := tunnelHTTPServ.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) { errChan <- fmt.Errorf("webserver tunnel listener failed: %v", err) } }() } - //Group the print statement so that multithreading wont disorder them + //Group the print statement so that multithreading won't disorder them log.Println("Started listening:\n", "\t\t\tTunnel Listener: ", tunnelListenAddress, "\n", "\t\t\tPublic Listener: ", config.Values.Webserver.Public.ListenAddress) @@ -381,7 +382,7 @@ func authorise(w http.ResponseWriter, r *http.Request) { mfaMethod.MFAPromptUI(w, r, user.Username, clientTunnelIp.String()) } -func reachability(w http.ResponseWriter, r *http.Request) { +func reachability(w http.ResponseWriter, _ *http.Request) { w.Header().Add("Content-Type", "text/plain") isDrained, err := data.IsDrained(data.GetServerID().String()) @@ -577,8 +578,8 @@ func registerDevice(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("type") == "mobile" { w.Header().Set("Content-Type", "text/html; charset=UTF-8") - var config bytes.Buffer - err = resources.RenderWithFuncs("interface.tmpl", &config, &wireguardInterface, template.FuncMap{ + var wireguardProfile bytes.Buffer + err = resources.RenderWithFuncs("interface.tmpl", &wireguardProfile, &wireguardInterface, template.FuncMap{ "StringsJoin": strings.Join, "Unescape": func(s string) template.HTML { return template.HTML(s) }, }) @@ -588,7 +589,7 @@ func registerDevice(w http.ResponseWriter, r *http.Request) { return } - image, err := qr.Encode(config.String(), qr.M, qr.Auto) + image, err := qr.Encode(wireguardProfile.String(), qr.M, qr.Auto) if err != nil { log.Println(username, remoteAddr, "failed to generate qr code:", err) http.Error(w, "Server Error", http.StatusInternalServerError) @@ -610,12 +611,12 @@ func registerDevice(w http.ResponseWriter, r *http.Request) { return } - qr := resources.QrCodeRegistrationDisplay{ + qrCodeBytes := resources.QrCodeRegistrationDisplay{ ImageData: template.URL("data:image/png;base64, " + base64.StdEncoding.EncodeToString(buff.Bytes())), Username: username, } - err = resources.Render("qrcode_registration.html", w, &qr) + err = resources.Render("qrcode_registration.html", w, &qrCodeBytes) if err != nil { log.Println(username, remoteAddr, "failed to execute template to show qr code wireguard config:", err) http.Error(w, "Server Error", http.StatusInternalServerError) @@ -697,7 +698,7 @@ func routes(w http.ResponseWriter, r *http.Request) { remoteAddress := utils.GetIPFromRequest(r) user, err := users.GetUserFromAddress(remoteAddress) if err != nil { - log.Println(user.Username, remoteAddress, "Could not find user: ", err) + log.Println("unknown", remoteAddress, "Could not find user: ", err) http.Error(w, "Server Error", http.StatusInternalServerError) return } @@ -724,7 +725,7 @@ func status(w http.ResponseWriter, r *http.Request) { remoteAddress := utils.GetIPFromRequest(r) user, err := users.GetUserFromAddress(remoteAddress) if err != nil { - log.Println(user.Username, remoteAddress, "Could not find user: ", err) + log.Println("unknown", remoteAddress, "Could not find user: ", err) http.Error(w, "Server Error", http.StatusInternalServerError) return } diff --git a/main.go b/main.go index b7ce62ec..05d16c2b 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "errors" "flag" "fmt" "log" @@ -70,7 +71,7 @@ func root(args []string) error { } if err := cmd.Check(); err != nil { - if err != flag.ErrHelp { + if errors.Is(err, flag.ErrHelp) { fmt.Println("Error: ", err.Error()) cmd.PrintUsage() } diff --git a/pkg/control/server/server.go b/pkg/control/server/server.go index 169a57fb..25321cb0 100644 --- a/pkg/control/server/server.go +++ b/pkg/control/server/server.go @@ -147,7 +147,7 @@ func StartControlSocket() error { Handler: controlMux, } - srv.Serve(l) + log.Println("failed to serve control socket: ", srv.Serve(l)) }() return nil } diff --git a/pkg/control/server/users.go b/pkg/control/server/users.go index e56a6755..c35a5d8b 100644 --- a/pkg/control/server/users.go +++ b/pkg/control/server/users.go @@ -43,13 +43,13 @@ func listUsers(w http.ResponseWriter, r *http.Request) { return } - users, err := data.GetAllUsers() + currentUsers, err := data.GetAllUsers() if err != nil { http.Error(w, err.Error(), 500) return } - b, err := json.Marshal(users) + b, err := json.Marshal(currentUsers) if err != nil { http.Error(w, err.Error(), 500) return @@ -186,13 +186,13 @@ func listAdminUsers(w http.ResponseWriter, r *http.Request) { return } - users, err := data.GetAllAdminUsers() + currentAdminUsers, err := data.GetAllAdminUsers() if err != nil { http.Error(w, err.Error(), 500) return } - b, err := json.Marshal(users) + b, err := json.Marshal(currentAdminUsers) if err != nil { http.Error(w, err.Error(), 500) return @@ -241,7 +241,11 @@ func unlockAdminUser(w http.ResponseWriter, r *http.Request) { username := r.FormValue("username") - data.SetAdminUserUnlock(username) + err = data.SetAdminUserUnlock(username) + if err != nil { + http.Error(w, err.Error(), 500) + return + } log.Println(username, "admin unlocked") diff --git a/pkg/control/wagctl/client.go b/pkg/control/wagctl/client.go index fbbda37a..69662600 100644 --- a/pkg/control/wagctl/client.go +++ b/pkg/control/wagctl/client.go @@ -54,7 +54,7 @@ func (c *CtrlClient) simplepost(path string, form url.Values) error { return nil } -// List devices, if the username field is empty (""), then list all devices. Otherwise list the one device corrosponding to the set username +// ListDevice if the username field is empty (""), then list all devices. Otherwise list the one device corrosponding to the set username func (c *CtrlClient) ListDevice(username string) (d []data.Device, err error) { response, err := c.httpClient.Get("http://unix/device/list?username=" + url.QueryEscape(username)) @@ -282,12 +282,12 @@ func (c *CtrlClient) GetPolicies() (result []control.PolicyData, err error) { // Add wag rule func (c *CtrlClient) AddPolicy(policies control.PolicyData) error { - data, err := json.Marshal(policies) + policiesData, err := json.Marshal(policies) if err != nil { return err } - response, err := c.httpClient.Post("http://unix/config/policy/create", "application/json", bytes.NewBuffer(data)) + response, err := c.httpClient.Post("http://unix/config/policy/create", "application/json", bytes.NewBuffer(policiesData)) if err != nil { return err } @@ -307,12 +307,12 @@ func (c *CtrlClient) AddPolicy(policies control.PolicyData) error { // Edit wag rule func (c *CtrlClient) EditPolicies(policy control.PolicyData) error { - data, err := json.Marshal(policy) + polciesData, err := json.Marshal(policy) if err != nil { return err } - response, err := c.httpClient.Post("http://unix/config/policy/edit", "application/json", bytes.NewBuffer(data)) + response, err := c.httpClient.Post("http://unix/config/policy/edit", "application/json", bytes.NewBuffer(polciesData)) if err != nil { return err } @@ -331,12 +331,12 @@ func (c *CtrlClient) EditPolicies(policy control.PolicyData) error { func (c *CtrlClient) RemovePolicies(policyNames []string) error { - data, err := json.Marshal(policyNames) + policiesData, err := json.Marshal(policyNames) if err != nil { return err } - response, err := c.httpClient.Post("http://unix/config/policies/delete", "application/json", bytes.NewBuffer(data)) + response, err := c.httpClient.Post("http://unix/config/policies/delete", "application/json", bytes.NewBuffer(policiesData)) if err != nil { return err } @@ -372,7 +372,7 @@ func (c *CtrlClient) GetGroups() (result []control.GroupData, err error) { // Add wag group/s func (c *CtrlClient) AddGroup(group control.GroupData) error { - data, err := json.Marshal(group) + groupData, err := json.Marshal(group) if err != nil { return err } @@ -381,7 +381,7 @@ func (c *CtrlClient) AddGroup(group control.GroupData) error { return errors.New("group did not have the 'group:' prefix") } - response, err := c.httpClient.Post("http://unix/config/group/create", "application/json", bytes.NewBuffer(data)) + response, err := c.httpClient.Post("http://unix/config/group/create", "application/json", bytes.NewBuffer(groupData)) if err != nil { return err } @@ -401,7 +401,7 @@ func (c *CtrlClient) AddGroup(group control.GroupData) error { // Edit wag group members func (c *CtrlClient) EditGroup(group control.GroupData) error { - data, err := json.Marshal(group) + groupData, err := json.Marshal(group) if err != nil { return err } @@ -410,7 +410,7 @@ func (c *CtrlClient) EditGroup(group control.GroupData) error { return errors.New("group did not have the 'group:' prefix") } - response, err := c.httpClient.Post("http://unix/config/group/edit", "application/json", bytes.NewBuffer(data)) + response, err := c.httpClient.Post("http://unix/config/group/edit", "application/json", bytes.NewBuffer(groupData)) if err != nil { return err } @@ -429,12 +429,12 @@ func (c *CtrlClient) EditGroup(group control.GroupData) error { func (c *CtrlClient) RemoveGroup(groupNames []string) error { - data, err := json.Marshal(groupNames) + groupData, err := json.Marshal(groupNames) if err != nil { return err } - response, err := c.httpClient.Post("http://unix/config/group/delete", "application/json", bytes.NewBuffer(data)) + response, err := c.httpClient.Post("http://unix/config/group/delete", "application/json", bytes.NewBuffer(groupData)) if err != nil { return err } diff --git a/ui/devices.go b/ui/devices.go index 62105134..c8c2335a 100644 --- a/ui/devices.go +++ b/ui/devices.go @@ -63,10 +63,10 @@ func devicesMgmt(w http.ResponseWriter, r *http.Request) { return } - data := []DevicesData{} + var deviceData []DevicesData for _, dev := range allDevices { - data = append(data, DevicesData{ + deviceData = append(deviceData, DevicesData{ Owner: dev.Username, Locked: dev.Attempts >= lockout, InternalIP: dev.Address, @@ -76,7 +76,7 @@ func devicesMgmt(w http.ResponseWriter, r *http.Request) { }) } - b, err := json.Marshal(data) + b, err := json.Marshal(deviceData) if err != nil { log.Println("unable to marshal devices data: ", err) diff --git a/ui/diagnostics.go b/ui/diagnostics.go index 6a4ee143..72dab093 100644 --- a/ui/diagnostics.go +++ b/ui/diagnostics.go @@ -105,7 +105,7 @@ func wgDiagnositicsData(w http.ResponseWriter, r *http.Request) { return } - data := []WgDevicesData{} + var wireguardPeers []WgDevicesData for _, peer := range peers { ip := "-" @@ -113,7 +113,7 @@ func wgDiagnositicsData(w http.ResponseWriter, r *http.Request) { ip = peer.AllowedIPs[0].String() } - data = append(data, WgDevicesData{ + wireguardPeers = append(wireguardPeers, WgDevicesData{ ReceiveBytes: peer.ReceiveBytes, TransmitBytes: peer.TransmitBytes, @@ -125,7 +125,7 @@ func wgDiagnositicsData(w http.ResponseWriter, r *http.Request) { }) } - result, err := json.Marshal(data) + result, err := json.Marshal(wireguardPeers) if err != nil { log.Println("unable to marshal peers data: ", err) http.Error(w, "Bad Request", http.StatusBadRequest) diff --git a/ui/groups.go b/ui/groups.go index ae4e97e1..1b7b8a43 100644 --- a/ui/groups.go +++ b/ui/groups.go @@ -76,6 +76,7 @@ func groups(w http.ResponseWriter, r *http.Request) { } w.Write([]byte("OK")) + return case "PUT": var group control.GroupData err := json.NewDecoder(r.Body).Decode(&group) @@ -92,6 +93,7 @@ func groups(w http.ResponseWriter, r *http.Request) { } w.Write([]byte("OK")) + return case "POST": var group control.GroupData err := json.NewDecoder(r.Body).Decode(&group) @@ -110,6 +112,7 @@ func groups(w http.ResponseWriter, r *http.Request) { } w.Write([]byte("OK")) + return default: http.NotFound(w, r) return diff --git a/ui/notifications.go b/ui/notifications.go index b147f46f..e11eb350 100644 --- a/ui/notifications.go +++ b/ui/notifications.go @@ -152,10 +152,10 @@ func startUpdateChecker(notifications chan<- Notification) { log.Println("unable to fetch updates: ", err) return } - defer resp.Body.Close() var gr githubResponse err = json.NewDecoder(resp.Body).Decode(&gr) + resp.Body.Close() if err != nil { log.Println("unable to parse update json: ", err) return diff --git a/ui/policies.go b/ui/policies.go index b47ee008..a8cfa7ac 100644 --- a/ui/policies.go +++ b/ui/policies.go @@ -77,6 +77,7 @@ func policies(w http.ResponseWriter, r *http.Request) { } w.Write([]byte("OK")) + return case "PUT": var group control.PolicyData err := json.NewDecoder(r.Body).Decode(&group) @@ -95,6 +96,7 @@ func policies(w http.ResponseWriter, r *http.Request) { } w.Write([]byte("OK")) + return case "POST": var policy control.PolicyData err := json.NewDecoder(r.Body).Decode(&policy) @@ -113,6 +115,7 @@ func policies(w http.ResponseWriter, r *http.Request) { } w.Write([]byte("OK")) + return default: http.NotFound(w, r) return diff --git a/ui/registration.go b/ui/registration.go index d460db54..caf2453d 100644 --- a/ui/registration.go +++ b/ui/registration.go @@ -59,10 +59,10 @@ func registrationTokens(w http.ResponseWriter, r *http.Request) { return } - data := []TokensData{} + var tokens []TokensData for _, reg := range registrations { - data = append(data, TokensData{ + tokens = append(tokens, TokensData{ Username: reg.Username, Token: reg.Token, Groups: reg.Groups, @@ -71,7 +71,7 @@ func registrationTokens(w http.ResponseWriter, r *http.Request) { }) } - b, err := json.Marshal(data) + b, err := json.Marshal(tokens) if err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return @@ -106,6 +106,7 @@ func registrationTokens(w http.ResponseWriter, r *http.Request) { } w.Write([]byte("OK")) + return case "POST": @@ -152,6 +153,7 @@ func registrationTokens(w http.ResponseWriter, r *http.Request) { } w.Write([]byte("OK")) + return default: http.NotFound(w, r) diff --git a/ui/security.go b/ui/security.go index 0c767181..726e2262 100644 --- a/ui/security.go +++ b/ui/security.go @@ -12,7 +12,7 @@ type security struct { func (sh *security) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("Strict-Transport-Security", "max-age=31536000") - w.Header().Set("ContentTypeNosniff", "nosniff") + w.Header().Set("X-Content-Type-Options", "nosniff") if r.Method != "GET" { u, err := url.Parse(r.Header.Get("Origin")) diff --git a/ui/ui_webserver.go b/ui/ui_webserver.go index 5daf9997..f86aff61 100644 --- a/ui/ui_webserver.go +++ b/ui/ui_webserver.go @@ -4,6 +4,7 @@ import ( "crypto/rand" "crypto/tls" "encoding/hex" + "errors" "fmt" "html" "html/template" @@ -84,7 +85,7 @@ func render(w http.ResponseWriter, r *http.Request, model interface{}, content . parsed, err = template.New(name).Funcs(funcsMap).ParseFS(templatesContent, content...) } else { - realFiles := []string{} + var realFiles []string for _, c := range content { realFiles = append(realFiles, filepath.Join("ui/", c)) } @@ -366,7 +367,7 @@ func StartWebServer(errs chan<- error) error { Handler: setSecurityHeaders(allRoutes), } - if err := HTTPSServer.ListenAndServeTLS(config.Values.ManagementUI.CertPath, config.Values.ManagementUI.KeyPath); err != nil && err != http.ErrServerClosed { + if err := HTTPSServer.ListenAndServeTLS(config.Values.ManagementUI.CertPath, config.Values.ManagementUI.KeyPath); err != nil && !errors.Is(err, http.ErrServerClosed) { errs <- fmt.Errorf("TLS management listener failed: %v", err) } @@ -380,7 +381,7 @@ func StartWebServer(errs chan<- error) error { IdleTimeout: 120 * time.Second, Handler: setSecurityHeaders(allRoutes), } - if err := HTTPServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + if err := HTTPServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { errs <- fmt.Errorf("webserver management listener failed: %v", HTTPServer.ListenAndServe()) } diff --git a/ui/users.go b/ui/users.go index b6889e8d..a2954082 100644 --- a/ui/users.go +++ b/ui/users.go @@ -61,7 +61,7 @@ func manageUsers(w http.ResponseWriter, r *http.Request) { return } - usersData := []UsersData{} + var usersData []UsersData for _, u := range users { devices, _ := ctrl.ListDevice(u.Username) @@ -90,6 +90,7 @@ func manageUsers(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write(b) + return case "PUT": var action struct { Action string `json:"action"` @@ -133,6 +134,7 @@ func manageUsers(w http.ResponseWriter, r *http.Request) { } w.Write([]byte("OK")) + return case "DELETE": var usernames []string @@ -160,6 +162,7 @@ func manageUsers(w http.ResponseWriter, r *http.Request) { } w.Write([]byte("OK")) + return default: http.NotFound(w, r) From 1a6fe486c35e59e1979b0a6ec79df5739d9637a0 Mon Sep 17 00:00:00 2001 From: NHAS Date: Wed, 15 May 2024 12:18:18 +1200 Subject: [PATCH 15/19] Set node association on startup, and on add peer, update node association on cluster informing that a client has roamed --- internal/router/bpf.go | 31 ++++++++++++++++++++++++++++++- internal/router/statemachine.go | 10 +++++++++- internal/router/wireguard.go | 4 ++-- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/internal/router/bpf.go b/internal/router/bpf.go index 9ee8277a..b85c2c25 100644 --- a/internal/router/bpf.go +++ b/internal/router/bpf.go @@ -168,7 +168,7 @@ func setupXDP(users []data.UserModel, knownDevices []data.Device) error { for _, device := range knownDevices { - err := xdpAddDevice(device.Username, device.Address, 0) + err := xdpAddDevice(device.Username, device.Address, uint64(device.AssociatedNode)) if err != nil { return errors.New("xdp setup add device to user: " + err.Error()) } @@ -627,6 +627,35 @@ func SetAuthorized(internalAddress, username string, node uint64) error { return xdpObjects.Devices.Update(net.ParseIP(internalAddress).To4(), deviceStruct.Bytes(), ebpf.UpdateExist) } +func UpdateNodeAssociation(deviceAddress string, node uint64) error { + lock.Lock() + defer lock.Unlock() + + ip := net.ParseIP(deviceAddress) + if ip == nil { + return errors.New("Unable to get IP address from: " + deviceAddress) + } + + if ip.To4() == nil { + return errors.New("IP address was not ipv4") + } + + deviceBytes, err := xdpObjects.Devices.LookupBytes(ip.To4()) + if err != nil { + return err + } + + var deviceStruct fwentry + err = deviceStruct.Unpack(deviceBytes) + if err != nil { + return err + } + + deviceStruct.associatedNode = node + + return xdpObjects.Devices.Update(ip.To4(), deviceStruct.Bytes(), ebpf.UpdateExist) +} + func Deauthenticate(address string) error { lock.Lock() diff --git a/internal/router/statemachine.go b/internal/router/statemachine.go index 3a3131c9..489186af 100644 --- a/internal/router/statemachine.go +++ b/internal/router/statemachine.go @@ -77,7 +77,7 @@ func deviceChanges(_ string, current, previous data.Device, et data.EventType) e case data.CREATED: key, _ := wgtypes.ParseKey(current.Publickey) - err := AddPeer(key, current.Username, current.Address, current.PresharedKey) + err := AddPeer(key, current.Username, current.Address, current.PresharedKey, uint64(current.AssociatedNode)) if err != nil { return fmt.Errorf("unable to create peer: %s: err: %s", current.Address, err) } @@ -111,6 +111,14 @@ func deviceChanges(_ string, current, previous data.Device, et data.EventType) e } + if current.AssociatedNode != previous.AssociatedNode { + err := UpdateNodeAssociation(current.Address, uint64(current.AssociatedNode)) + if err != nil { + return fmt.Errorf("cannot change device node association %s:%s: %s", current.Address, current.Username, err) + } + log.Printf("changed device (%s:%s) node association: %s -> %s", current.Address, current.Username, previous.AssociatedNode, current.AssociatedNode) + } + // If the authorisation state has changed and is not disabled if current.Authorised != previous.Authorised && !current.Authorised.IsZero() { if current.Attempts <= lockout && current.AssociatedNode == previous.AssociatedNode { diff --git a/internal/router/wireguard.go b/internal/router/wireguard.go index 74783f5a..79312e84 100644 --- a/internal/router/wireguard.go +++ b/internal/router/wireguard.go @@ -250,7 +250,7 @@ func ListPeers() ([]wgtypes.Peer, error) { } // AddPeer adds the device to wireguard -func AddPeer(public wgtypes.Key, username, addresss, presharedKey string) (err error) { +func AddPeer(public wgtypes.Key, username, addresss, presharedKey string, node uint64) (err error) { lock.Lock() defer lock.Unlock() @@ -275,7 +275,7 @@ func AddPeer(public wgtypes.Key, username, addresss, presharedKey string) (err e }, } - err = xdpAddDevice(username, addresss, 0) + err = xdpAddDevice(username, addresss, node) if err != nil { return err } From 0ff790306182db688b92b9a5e1f86beeade4f46f Mon Sep 17 00:00:00 2001 From: NHAS Date: Wed, 15 May 2024 12:20:52 +1200 Subject: [PATCH 16/19] Add associated node to firewall details diagnostic --- internal/router/bpf.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/router/bpf.go b/internal/router/bpf.go index b85c2c25..3860bdae 100644 --- a/internal/router/bpf.go +++ b/internal/router/bpf.go @@ -716,6 +716,7 @@ type fwDevice struct { Expiry uint64 IP string Authorized bool + AssociatedNode string } func GetRoutes(username string) ([]string, error) { @@ -831,7 +832,13 @@ func GetRules() (map[string]FirewallRules, error) { } fwRule := result[res] - fwRule.Devices = append(fwRule.Devices, fwDevice{IP: net.IP(ipBytes).String(), Authorized: isAuthed(net.IP(ipBytes).String()), Expiry: deviceStruct.sessionExpiry, LastPacketTimestamp: deviceStruct.lastPacketTime}) + fwRule.Devices = append(fwRule.Devices, fwDevice{ + IP: net.IP(ipBytes).String(), + Authorized: isAuthed(net.IP(ipBytes).String()), + Expiry: deviceStruct.sessionExpiry, + LastPacketTimestamp: deviceStruct.lastPacketTime, + AssociatedNode: fmt.Sprintf("%x (%d)", deviceStruct.associatedNode, deviceStruct.associatedNode), + }) if err := xdpObjects.AccountLocked.Lookup(deviceStruct.user_id, &fwRule.AccountLocked); err != nil { log.Println("[ERROR] User ID was not properly in firewall map: ", hex.EncodeToString(deviceStruct.user_id[:]), " err: ", err) From 98d1925242fca4a3614e5feeb185870fe5ac75fd Mon Sep 17 00:00:00 2001 From: NHAS Date: Wed, 15 May 2024 13:26:57 +1200 Subject: [PATCH 17/19] Allow roaming between cluster members if endpoint ip stays the same --- internal/router/bpf.go | 17 +++++++++-------- internal/router/init.go | 28 +++++++++++++++++++--------- internal/router/statemachine.go | 4 ++-- internal/router/wireguard.go | 29 ++++++++++++++++++++++++++++- 4 files changed, 58 insertions(+), 20 deletions(-) diff --git a/internal/router/bpf.go b/internal/router/bpf.go index 3860bdae..33611d6a 100644 --- a/internal/router/bpf.go +++ b/internal/router/bpf.go @@ -627,18 +627,19 @@ func SetAuthorized(internalAddress, username string, node uint64) error { return xdpObjects.Devices.Update(net.ParseIP(internalAddress).To4(), deviceStruct.Bytes(), ebpf.UpdateExist) } -func UpdateNodeAssociation(deviceAddress string, node uint64) error { +func UpdateNodeAssociation(device data.Device) error { lock.Lock() defer lock.Unlock() - ip := net.ParseIP(deviceAddress) - if ip == nil { - return errors.New("Unable to get IP address from: " + deviceAddress) + // If the peer roams away from us, unset the endpoint in wireguard to make sure the peer watcher will absolutely register a change if they roam back + if device.AssociatedNode != data.GetServerID() { + err := setPeerEndpoint(device, &net.UDPAddr{}) + if err != nil { + return err + } } - if ip.To4() == nil { - return errors.New("IP address was not ipv4") - } + ip := net.ParseIP(device.Address) deviceBytes, err := xdpObjects.Devices.LookupBytes(ip.To4()) if err != nil { @@ -651,7 +652,7 @@ func UpdateNodeAssociation(deviceAddress string, node uint64) error { return err } - deviceStruct.associatedNode = node + deviceStruct.associatedNode = uint64(device.AssociatedNode) return xdpObjects.Devices.Update(ip.To4(), deviceStruct.Bytes(), ebpf.UpdateExist) } diff --git a/internal/router/init.go b/internal/router/init.go index 816d555b..30f364df 100644 --- a/internal/router/init.go +++ b/internal/router/init.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "log" + "net" "strings" "sync" "time" @@ -20,6 +21,10 @@ var ( cancel = make(chan bool) ) +var ( + EmptyEndpoint = &net.UDPAddr{} +) + func Setup(errorChan chan<- error, iptables bool) (err error) { initialUsers, knownDevices, err := data.GetInitialData() @@ -53,7 +58,6 @@ func Setup(errorChan chan<- error, iptables bool) (err error) { handleEvents(errorChan) go func() { - ourPeerAddresses := make(map[string]string) for { @@ -85,22 +89,28 @@ func Setup(errorChan chan<- error, iptables bool) (err error) { continue } - if _, ok := ourPeerAddresses[device.Address]; !ok { + // If the peer endpoint has become empty (due to peer roaming) or if we dont have a record of it, set the map + if _, ok := ourPeerAddresses[device.Address]; !ok || EmptyEndpoint.String() == p.Endpoint.String() { ourPeerAddresses[device.Address] = p.Endpoint.String() } - if ourPeerAddresses[device.Address] != p.Endpoint.String() { - + // If the peer address has changed, but is not empty (empty indicates the peer has changed it node association away from this node) + if ourPeerAddresses[device.Address] != p.Endpoint.String() && ourPeerAddresses[device.Address] != EmptyEndpoint.String() { ourPeerAddresses[device.Address] = p.Endpoint.String() - log.Printf("%s:%s endpoint changed %s -> %s", device.Address, device.Username, device.Endpoint.String(), p.Endpoint.String()) + // If we register an endpoint change on our real world device, and the Endpoint is not the same as what the cluster knows + // i.e the peer has either roamed and its egress has changed, or it's an attacker using a stolen wireguard profile + // Deauthenticate it + if device.Endpoint.String() != p.Endpoint.String() { + log.Printf("%s:%s endpoint changed %s -> %s", device.Address, device.Username, device.Endpoint.String(), p.Endpoint.String()) - err = data.DeauthenticateDevice(device.Address) - if err != nil { - log.Printf("failed to deauth device (%s:%s) endpoint: %s", device.Address, device.Username, err) + err = data.DeauthenticateDevice(device.Address) + if err != nil { + log.Printf("failed to deauth device (%s:%s) endpoint: %s", device.Address, device.Username, err) + } } - // This will set the association to this node automatically + // Otherwise, just update the node association err = data.UpdateDeviceConnectionDetails(p.AllowedIPs[0].IP.String(), p.Endpoint) if err != nil { log.Printf("unable to update device (%s:%s) endpoint: %s", device.Address, device.Username, err) diff --git a/internal/router/statemachine.go b/internal/router/statemachine.go index 489186af..d7687913 100644 --- a/internal/router/statemachine.go +++ b/internal/router/statemachine.go @@ -101,7 +101,6 @@ func deviceChanges(_ string, current, previous data.Device, et data.EventType) e if current.Attempts > lockout || // If the number of authentication attempts on a device has exceeded the max current.Endpoint.String() != previous.Endpoint.String() || // If the client ip has changed - current.AssociatedNode != previous.AssociatedNode || // If the node the client was sending to is now different current.Authorised.IsZero() { // If we've explicitly deauthorised a device err := Deauthenticate(current.Address) if err != nil { @@ -112,10 +111,11 @@ func deviceChanges(_ string, current, previous data.Device, et data.EventType) e } if current.AssociatedNode != previous.AssociatedNode { - err := UpdateNodeAssociation(current.Address, uint64(current.AssociatedNode)) + err := UpdateNodeAssociation(current) if err != nil { return fmt.Errorf("cannot change device node association %s:%s: %s", current.Address, current.Username, err) } + log.Printf("changed device (%s:%s) node association: %s -> %s", current.Address, current.Username, previous.AssociatedNode, current.AssociatedNode) } diff --git a/internal/router/wireguard.go b/internal/router/wireguard.go index 79312e84..5a5b5b1e 100644 --- a/internal/router/wireguard.go +++ b/internal/router/wireguard.go @@ -96,10 +96,13 @@ func setupWireguard(devices []data.Device) error { PublicKey: pk, ReplaceAllowedIPs: true, AllowedIPs: []net.IPNet{*network}, - Endpoint: device.Endpoint, PresharedKey: psk, } + if device.AssociatedNode == data.GetServerID() { + pc.Endpoint = device.Endpoint + } + if config.Values.Wireguard.ServerPersistentKeepAlive > 0 { d := time.Duration(config.Values.Wireguard.ServerPersistentKeepAlive) * time.Second pc.PersistentKeepaliveInterval = &d @@ -236,6 +239,30 @@ func ReplacePeer(device data.Device, newPublicKey wgtypes.Key) error { return nil } +func setPeerEndpoint(device data.Device, endpoint *net.UDPAddr) error { + + id, err := wgtypes.ParseKey(device.Publickey) + if err != nil { + return err + } + + var c wgtypes.Config + c.Peers = []wgtypes.PeerConfig{ + { + UpdateOnly: true, + PublicKey: id, + Endpoint: endpoint, + }, + } + + err = ctrl.ConfigureDevice(config.Values.Wireguard.DevName, c) + if err != nil { + return err + } + + return nil +} + func ListPeers() ([]wgtypes.Peer, error) { lock.Lock() From 40d51d629c94233fe157bdfdc629901b19bb6ff4 Mon Sep 17 00:00:00 2001 From: NHAS Date: Wed, 15 May 2024 13:31:18 +1200 Subject: [PATCH 18/19] Use nil instead of empty udp address --- internal/router/bpf.go | 2 +- internal/router/init.go | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/internal/router/bpf.go b/internal/router/bpf.go index 33611d6a..1cf63f7c 100644 --- a/internal/router/bpf.go +++ b/internal/router/bpf.go @@ -633,7 +633,7 @@ func UpdateNodeAssociation(device data.Device) error { // If the peer roams away from us, unset the endpoint in wireguard to make sure the peer watcher will absolutely register a change if they roam back if device.AssociatedNode != data.GetServerID() { - err := setPeerEndpoint(device, &net.UDPAddr{}) + err := setPeerEndpoint(device, nil) if err != nil { return err } diff --git a/internal/router/init.go b/internal/router/init.go index 30f364df..30088ffc 100644 --- a/internal/router/init.go +++ b/internal/router/init.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "log" - "net" "strings" "sync" "time" @@ -21,10 +20,6 @@ var ( cancel = make(chan bool) ) -var ( - EmptyEndpoint = &net.UDPAddr{} -) - func Setup(errorChan chan<- error, iptables bool) (err error) { initialUsers, knownDevices, err := data.GetInitialData() @@ -90,12 +85,12 @@ func Setup(errorChan chan<- error, iptables bool) (err error) { } // If the peer endpoint has become empty (due to peer roaming) or if we dont have a record of it, set the map - if _, ok := ourPeerAddresses[device.Address]; !ok || EmptyEndpoint.String() == p.Endpoint.String() { + if _, ok := ourPeerAddresses[device.Address]; !ok || p.Endpoint == nil { ourPeerAddresses[device.Address] = p.Endpoint.String() } // If the peer address has changed, but is not empty (empty indicates the peer has changed it node association away from this node) - if ourPeerAddresses[device.Address] != p.Endpoint.String() && ourPeerAddresses[device.Address] != EmptyEndpoint.String() { + if ourPeerAddresses[device.Address] != p.Endpoint.String() && ourPeerAddresses[device.Address] != "" { ourPeerAddresses[device.Address] = p.Endpoint.String() // If we register an endpoint change on our real world device, and the Endpoint is not the same as what the cluster knows From 2b10f0fc7dd50b9f83bbf2f968952081344fd7d9 Mon Sep 17 00:00:00 2001 From: NHAS Date: Wed, 15 May 2024 14:10:03 +1200 Subject: [PATCH 19/19] Add small ui indicator for witness nodes, disallow setting drain on witness node --- commands/start.go | 11 +++++--- internal/data/clustering.go | 46 +++++++++++++++++++++++++++---- internal/data/events.go | 4 +-- internal/data/init.go | 6 ++++ ui/clustering.go | 11 +++++++- ui/templates/cluster/members.html | 4 ++- 6 files changed, 68 insertions(+), 14 deletions(-) diff --git a/commands/start.go b/commands/start.go index 65949ebf..fe4e24e8 100644 --- a/commands/start.go +++ b/commands/start.go @@ -125,11 +125,14 @@ func clusterState(noIptables bool, errorChan chan<- error) func(string) { switch stateText { case "dead": if !wasDead { - log.Println("Tearing down node") - teardown(false) - - log.Println("Tear down complete") + if !config.Values.Clustering.Witness { + log.Println("Tearing down node") + teardown(false) + log.Println("Tear down complete") + } else { + log.Println("refusing to tear down witness node (nothing to tear down)") + } // Only teardown if we were at one point alive wasDead = true diff --git a/internal/data/clustering.go b/internal/data/clustering.go index 8272e995..2545b7a4 100644 --- a/internal/data/clustering.go +++ b/internal/data/clustering.go @@ -69,7 +69,7 @@ func GetLastPing(idHex string) (time.Time, error) { return time.Time{}, errors.New("id is not part of cluster") } - lastPing, err := etcd.Get(context.Background(), path.Join(NodeEvents, idHex, "ping")) + lastPing, err := etcd.Get(context.Background(), path.Join(NodeInfo, idHex, "ping")) if err != nil { return time.Time{}, err } @@ -90,18 +90,52 @@ func GetLastPing(idHex string) (time.Time, error) { return t, nil } -func SetDrained(idHex string, on bool) error { +func SetWitness(on bool) error { + if on { + _, err := etcd.Put(context.Background(), path.Join(NodeInfo, GetServerID().String(), "witness"), fmt.Sprintf("%t", on)) + return err + } + + _, err := etcd.Delete(context.Background(), path.Join(NodeInfo, GetServerID().String(), "witness")) + return err +} + +func IsWitness(idHex string) (bool, error) { _, err := strconv.ParseUint(idHex, 16, 64) + if err != nil { + return false, fmt.Errorf("bad member ID arg (%v), expecting ID in Hex", err) + } + + isDrained, err := etcd.Get(context.Background(), path.Join(NodeInfo, idHex, "witness")) + if err != nil { + return false, err + } + + return isDrained.Count != 0, nil +} + +func SetDrained(idHex string, on bool) error { + + isWitness, err := IsWitness(idHex) + if err != nil { + return err + } + + if isWitness { + return errors.New("cannot set drained on witness node, this node is not serving clients") + } + + _, err = strconv.ParseUint(idHex, 16, 64) if err != nil { return err } if on { - _, err = etcd.Put(context.Background(), path.Join(NodeEvents, idHex, "drain"), fmt.Sprintf("%t", on)) + _, err = etcd.Put(context.Background(), path.Join(NodeInfo, idHex, "drain"), fmt.Sprintf("%t", on)) return err } - _, err = etcd.Delete(context.Background(), path.Join(NodeEvents, idHex, "drain")) + _, err = etcd.Delete(context.Background(), path.Join(NodeInfo, idHex, "drain")) return err } @@ -111,7 +145,7 @@ func IsDrained(idHex string) (bool, error) { return false, fmt.Errorf("bad member ID arg (%v), expecting ID in Hex", err) } - isDrained, err := etcd.Get(context.Background(), path.Join(NodeEvents, idHex, "drain")) + isDrained, err := etcd.Get(context.Background(), path.Join(NodeInfo, idHex, "drain")) if err != nil { return false, err } @@ -230,7 +264,7 @@ func RemoveMember(idHex string) error { } // Clear any node metadata - _, err = etcd.Delete(context.Background(), path.Join(NodeEvents, idHex), clientv3.WithPrefix()) + _, err = etcd.Delete(context.Background(), path.Join(NodeInfo, idHex), clientv3.WithPrefix()) if err != nil { return err } diff --git a/internal/data/events.go b/internal/data/events.go index 37338ef8..895cdb88 100644 --- a/internal/data/events.go +++ b/internal/data/events.go @@ -43,7 +43,7 @@ const ( GroupsPrefix = "wag-groups-" ConfigPrefix = "wag-config-" AuthenticationPrefix = "wag-config-authentication-" - NodeEvents = "wag/node/" + NodeInfo = "wag/node/" NodeErrors = "wag/node/errors" ) @@ -208,7 +208,7 @@ func checkClusterHealth() { func testCluster() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - _, err := etcd.Put(ctx, path.Join(NodeEvents, GetServerID().String(), "ping"), time.Now().Format(time.RFC1123Z)) + _, err := etcd.Put(ctx, path.Join(NodeInfo, GetServerID().String(), "ping"), time.Now().Format(time.RFC1123Z)) cancel() if err != nil { log.Println("unable to write liveness value") diff --git a/internal/data/init.go b/internal/data/init.go index 69289b28..2a325b1e 100644 --- a/internal/data/init.go +++ b/internal/data/init.go @@ -193,6 +193,12 @@ func Load(path, joinToken string, testing bool) error { } + if config.Values.Clustering.Witness { + + } else { + + } + go checkClusterHealth() return nil diff --git a/ui/clustering.go b/ui/clustering.go index 859d12e7..0b0e8494 100644 --- a/ui/clustering.go +++ b/ui/clustering.go @@ -14,6 +14,7 @@ import ( type MembershipDTO struct { *membership.Member IsDrained bool + IsWitness bool Ping string Status string @@ -55,7 +56,14 @@ func clusterMembersUI(w http.ResponseWriter, r *http.Request) { for i := range data.GetMembers() { drained, err := data.IsDrained(members[i].ID.String()) if err != nil { - log.Println("unable to render clustering page: ", err) + log.Println("unable to get drained state: ", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + witness, err := data.IsWitness(members[i].ID.String()) + if err != nil { + log.Println("unable to witness state: ", err) w.WriteHeader(http.StatusInternalServerError) return } @@ -92,6 +100,7 @@ func clusterMembersUI(w http.ResponseWriter, r *http.Request) { d.Members = append(d.Members, MembershipDTO{ Member: members[i], IsDrained: drained, + IsWitness: witness, Status: status, Ping: ping, }) diff --git a/ui/templates/cluster/members.html b/ui/templates/cluster/members.html index 7f0a189b..f08989bf 100755 --- a/ui/templates/cluster/members.html +++ b/ui/templates/cluster/members.html @@ -46,7 +46,7 @@
{{if Role:
- {{if eq .ID $.Leader}}Leader{{else if .IsLearner}}Learner{{else}}Member{{end}} + {{if eq .ID $.Leader}}Leader{{else if .IsLearner}}Learner{{else if .IsWitness}}Witness{{else}}Member{{end}}