From 2bba0ddd7444ae5d518aefcf4fd14f3b8f411e4e Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 14 May 2025 02:00:20 +0200 Subject: [PATCH] might actually happen? Signed-off-by: Jens Langhammer --- go.mod | 1 + go.sum | 2 + internal/outpost/radius/api.go | 2 + internal/outpost/radius/eap/debug/debug.go | 21 +++ internal/outpost/radius/eap/handler.go | 80 +++++++++++ internal/outpost/radius/eap/packet.go | 89 ++++++++++++ .../outpost/radius/eap/payload_identity.go | 14 ++ internal/outpost/radius/eap/state.go | 26 ++++ internal/outpost/radius/eap/tls/conn.go | 48 +++++++ internal/outpost/radius/eap/tls/flags.go | 11 ++ internal/outpost/radius/eap/tls/payload.go | 133 ++++++++++++++++++ internal/outpost/radius/eap/tls/state.go | 19 +++ .../outpost/radius/handle_access_request.go | 39 +++++ internal/outpost/radius/radius.go | 2 + notes.md | 4 + 15 files changed, 491 insertions(+) create mode 100644 internal/outpost/radius/eap/debug/debug.go create mode 100644 internal/outpost/radius/eap/handler.go create mode 100644 internal/outpost/radius/eap/packet.go create mode 100644 internal/outpost/radius/eap/payload_identity.go create mode 100644 internal/outpost/radius/eap/state.go create mode 100644 internal/outpost/radius/eap/tls/conn.go create mode 100644 internal/outpost/radius/eap/tls/flags.go create mode 100644 internal/outpost/radius/eap/tls/payload.go create mode 100644 internal/outpost/radius/eap/tls/state.go create mode 100644 notes.md diff --git a/go.mod b/go.mod index ba1b568877..e19e6c2bdd 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/go-ldap/ldap/v3 v3.4.11 github.com/go-openapi/runtime v0.28.0 github.com/golang-jwt/jwt/v5 v5.2.2 + github.com/google/gopacket v1.1.19 github.com/google/uuid v1.6.0 github.com/gorilla/handlers v1.5.2 github.com/gorilla/mux v1.8.1 diff --git a/go.sum b/go.sum index 62105a740c..244c6d9442 100644 --- a/go.sum +++ b/go.sum @@ -154,6 +154,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= diff --git a/internal/outpost/radius/api.go b/internal/outpost/radius/api.go index 947fb7bf94..0f5672f486 100644 --- a/internal/outpost/radius/api.go +++ b/internal/outpost/radius/api.go @@ -9,6 +9,7 @@ import ( log "github.com/sirupsen/logrus" "goauthentik.io/internal/outpost/ak" + "goauthentik.io/internal/outpost/radius/eap" ) func parseCIDRs(raw string) []*net.IPNet { @@ -53,6 +54,7 @@ func (rs *RadiusServer) Refresh() error { providerId: provider.Pk, s: rs, log: logger, + eapState: map[string]*eap.State{}, } } rs.providers = providers diff --git a/internal/outpost/radius/eap/debug/debug.go b/internal/outpost/radius/eap/debug/debug.go new file mode 100644 index 0000000000..182b40926d --- /dev/null +++ b/internal/outpost/radius/eap/debug/debug.go @@ -0,0 +1,21 @@ +package debug + +import ( + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + log "github.com/sirupsen/logrus" + "layeh.com/radius" +) + +func DebugPacket(p *radius.Packet) { + log.Debug(p) + log.Debug(p.Attributes) + n, _ := p.Encode() + log.Debug(n) + packet := gopacket.NewPacket(n, layers.LayerTypeRADIUS, gopacket.Default) + layer := packet.Layer(layers.LayerTypeRADIUS) + if layer == nil { + return + } + log.Debug(layer.(*layers.RADIUS)) +} diff --git a/internal/outpost/radius/eap/handler.go b/internal/outpost/radius/eap/handler.go new file mode 100644 index 0000000000..0c5d130554 --- /dev/null +++ b/internal/outpost/radius/eap/handler.go @@ -0,0 +1,80 @@ +package eap + +import ( + "crypto/hmac" + "crypto/md5" + "encoding/base64" + + "github.com/gorilla/securecookie" + "goauthentik.io/internal/outpost/radius/eap/tls" + "layeh.com/radius" + "layeh.com/radius/rfc2865" + "layeh.com/radius/rfc2869" +) + +func (p *Packet) Handle(stm StateManager, w radius.ResponseWriter, r *radius.Packet) { + rst := rfc2865.State_GetString(r) + if rst == "" { + rst = base64.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(12)) + } + st := stm.GetEAPState(rst) + if st == nil { + st = BlankState(stm.GetEAPSettings()) + } + if len(st.ChallengesToOffer) < 1 { + panic("No more challenges") + } + nextChallengeToOffer := st.ChallengesToOffer[0] + res, newState := p.GetChallengeForType(st, nextChallengeToOffer) + stm.SetEAPState(rst, newState) + + rres := r.Response(radius.CodeAccessChallenge) + rfc2865.State_SetString(rres, rst) + eapEncoded, err := res.Encode() + if err != nil { + panic(err) + } + rfc2869.EAPMessage_Set(rres, eapEncoded) + p.setMessageAuthenticator(rres) + // debug.DebugPacket(rres) + err = w.Write(rres) + if err != nil { + panic(err) + } +} + +func (p *Packet) GetChallengeForType(st *State, t Type) (*Packet, *State) { + res := &Packet{ + code: CodeRequest, + id: p.id + 1, + msgType: t, + } + var payload any + var tst any + switch t { + case TypeTLS: + cp := tls.Payload{} + cp.Decode(p.rawPayload) + payload, tst = cp.Handle(st.TypeState[t]) + } + st.TypeState[t] = tst + res.Payload = payload.(Payload) + return res, st +} + +func (p *Packet) setMessageAuthenticator(rp *radius.Packet) { + err := rfc2869.MessageAuthenticator_Set(rp, make([]byte, 16)) + if err != nil { + panic(err) + } + hash := hmac.New(md5.New, rp.Secret) + encode, err := rp.MarshalBinary() + if err != nil { + panic(err) + } + hash.Write(encode) + err = rfc2869.MessageAuthenticator_Set(rp, hash.Sum(nil)) + if err != nil { + panic(err) + } +} diff --git a/internal/outpost/radius/eap/packet.go b/internal/outpost/radius/eap/packet.go new file mode 100644 index 0000000000..a914e307ad --- /dev/null +++ b/internal/outpost/radius/eap/packet.go @@ -0,0 +1,89 @@ +package eap + +import ( + "encoding/binary" + "errors" + "fmt" + + "goauthentik.io/internal/outpost/radius/eap/tls" +) + +type Code uint8 + +const ( + CodeRequest Code = 1 + CodeResponse Code = 2 +) + +type Type uint8 + +const ( + TypeIdentity Type = 1 + TypeMD5Challenge Type = 4 + TypeTLS Type = 13 +) + +type Packet struct { + code Code + id uint8 + length uint16 + msgType Type + rawPayload []byte + Payload Payload +} + +type Payload interface { + Decode(raw []byte) error + Encode() ([]byte, error) +} + +type PayloadWriter struct{} + +func emptyPayload(t Type) Payload { + switch t { + case TypeIdentity: + return &IdentityPayload{} + case TypeTLS: + return &tls.Payload{} + } + return nil +} + +func Decode(raw []byte) (*Packet, error) { + packet := &Packet{} + packet.code = Code(raw[0]) + packet.id = raw[1] + packet.length = binary.BigEndian.Uint16(raw[2:]) + if packet.length != uint16(len(raw)) { + return nil, errors.New("mismatched packet length") + } + if len(raw) > 4 && (packet.code == CodeRequest || packet.code == CodeResponse) { + packet.msgType = Type(raw[4]) + } + packet.Payload = emptyPayload(packet.msgType) + packet.rawPayload = raw[5:] + fmt.Printf("decode raw '% x\n", raw[5:]) + err := packet.Payload.Decode(raw[5:]) + if err != nil { + return nil, err + } + return packet, nil +} + +func (p *Packet) Encode() ([]byte, error) { + buff := make([]byte, 5) + buff[0] = uint8(p.code) + buff[1] = uint8(p.id) + + payloadBuffer, err := p.Payload.Encode() + if err != nil { + return buff, err + } + binary.BigEndian.PutUint16(buff[2:], uint16(len(payloadBuffer)+5)) + + if p.code == CodeRequest || p.code == CodeResponse { + buff[4] = uint8(p.msgType) + } + buff = append(buff, payloadBuffer...) + return buff, nil +} diff --git a/internal/outpost/radius/eap/payload_identity.go b/internal/outpost/radius/eap/payload_identity.go new file mode 100644 index 0000000000..6eb22cc107 --- /dev/null +++ b/internal/outpost/radius/eap/payload_identity.go @@ -0,0 +1,14 @@ +package eap + +type IdentityPayload struct { + Identity string +} + +func (ip *IdentityPayload) Decode(raw []byte) error { + ip.Identity = string(raw) + return nil +} + +func (ip *IdentityPayload) Encode() ([]byte, error) { + panic("Identity encode") +} diff --git a/internal/outpost/radius/eap/state.go b/internal/outpost/radius/eap/state.go new file mode 100644 index 0000000000..6c1b65dad6 --- /dev/null +++ b/internal/outpost/radius/eap/state.go @@ -0,0 +1,26 @@ +package eap + +import "slices" + +type Settings struct { + ChallengesToOffer []Type + ChallengeSettings map[Type]interface{} +} + +type StateManager interface { + GetEAPSettings() Settings + GetEAPState(string) *State + SetEAPState(string, *State) +} + +type State struct { + ChallengesToOffer []Type + TypeState map[Type]any +} + +func BlankState(settings Settings) *State { + return &State{ + ChallengesToOffer: slices.Clone(settings.ChallengesToOffer), + TypeState: map[Type]any{}, + } +} diff --git a/internal/outpost/radius/eap/tls/conn.go b/internal/outpost/radius/eap/tls/conn.go new file mode 100644 index 0000000000..342131fcfa --- /dev/null +++ b/internal/outpost/radius/eap/tls/conn.go @@ -0,0 +1,48 @@ +package tls + +import ( + "bytes" + "net" + "time" +) + +type TLSConnection struct { + reader *bytes.Buffer + writer *bytes.Buffer +} + +func NewTLSConnection(initialData []byte) TLSConnection { + c := TLSConnection{ + reader: bytes.NewBuffer(initialData), + writer: bytes.NewBuffer([]byte{}), + } + // e.Request.Log().WithField("tls", len(c.reader.Bytes())).Debug("TLS Early") + return c +} + +// func (conn *TLSConnection) SetCode(code radius.Code) { +// conn.code = code +// } +func (conn TLSConnection) Read(p []byte) (int, error) { return conn.reader.Read(p) } +func (conn TLSConnection) Write(p []byte) (int, error) { + // final := make([]byte, 1) + // final[0] = 128 // TLS Flags + // final = append(final, p...) + return conn.writer.Write(p) + // return 0, nil + // return conn.EAPConnection.Write(conn.code, final) +} +func (conn TLSConnection) Close() error { return nil } +func (conn TLSConnection) LocalAddr() net.Addr { return nil } +func (conn TLSConnection) RemoteAddr() net.Addr { return nil } +func (conn TLSConnection) SetDeadline(t time.Time) error { return nil } +func (conn TLSConnection) SetReadDeadline(t time.Time) error { return nil } +func (conn TLSConnection) SetWriteDeadline(t time.Time) error { return nil } + +func (conn TLSConnection) TLSData() []byte { + return conn.writer.Bytes() +} + +// func (conn TLSConnection) ContentType() layers.TLSType { +// return layers.TLSType(conn.TypeData[1]) +// } diff --git a/internal/outpost/radius/eap/tls/flags.go b/internal/outpost/radius/eap/tls/flags.go new file mode 100644 index 0000000000..b09d3c3a8b --- /dev/null +++ b/internal/outpost/radius/eap/tls/flags.go @@ -0,0 +1,11 @@ +package tls + +type Flag byte + +const ( + FlagLengthIncluded Flag = 1 << 7 + FlagMoreFragments Flag = 1 << 6 + FlagTLSStart Flag = 1 << 5 + FlagNone Flag = 0 + FlagLengthMore Flag = 0xc0 +) diff --git a/internal/outpost/radius/eap/tls/payload.go b/internal/outpost/radius/eap/tls/payload.go new file mode 100644 index 0000000000..586e86e161 --- /dev/null +++ b/internal/outpost/radius/eap/tls/payload.go @@ -0,0 +1,133 @@ +package tls + +import ( + "crypto/tls" + "encoding/binary" + "errors" + "fmt" + "slices" +) + +type Payload struct { + Flags Flag + Length uint32 + Data []byte +} + +func (p *Payload) Decode(raw []byte) error { + p.Flags = Flag(raw[0]) + if p.Flags&FlagLengthIncluded != 0 { + if len(raw) < 4 { + return errors.New("invalid size") + } + p.Length = binary.BigEndian.Uint32(raw) + p.Data = raw[5:] + } + return nil +} + +func (p *Payload) Encode() ([]byte, error) { + l := 1 + if p.Flags&FlagLengthIncluded != 0 { + l += 4 + } + buff := make([]byte, len(p.Data)+l) + buff[0] = byte(p.Flags) + if p.Flags&FlagLengthIncluded != 0 { + buff[1] = byte(p.Length >> 24) + buff[2] = byte(p.Length >> 16) + buff[3] = byte(p.Length >> 8) + buff[4] = byte(p.Length) + } + if len(p.Data) > 0 { + copy(buff[5:], p.Data) + } + return buff, nil +} + +var certs = []tls.Certificate{} + +func init() { + // Testing + cert, err := tls.LoadX509KeyPair( + "../t/ca/out/cert_jens-mbp.lab.beryju.org.pem", + "../t/ca/out/cert_jens-mbp.lab.beryju.org.key", + ) + if err != nil { + panic(err) + } + certs = append(certs, cert) +} + +func (p *Payload) Handle(stt any) (*Payload, State) { + if stt == nil { + stt = NewState() + } + st := stt.(State) + fmt.Printf("Got TLS packet % x\n", p.Flags) + if !st.HasStarted { + st.HasStarted = true + return &Payload{ + Flags: FlagTLSStart, + }, st + } + if st.HasMore() { + return p.sendNextChunk(st) + } + + fmt.Printf("decode tls raw '% x\n", p.Data) + + tc := NewTLSConnection(p.Data) + if st.TLS == nil { + fmt.Printf("no TLS connection in state yet, starting connection") + st.TLS = tls.Server(tc, &tls.Config{ + GetConfigForClient: func(argHello *tls.ClientHelloInfo) (*tls.Config, error) { + fmt.Printf("%+v\n", argHello) + return nil, nil + }, + ClientAuth: tls.RequireAnyClientCert, + Certificates: certs, + }) + st.TLS.Handshake() + } + return p.sendDataChunked(tc.TLSData(), st) +} + +const maxChunkSize = 1000 + +func (p *Payload) sendDataChunked(data []byte, st State) (*Payload, State) { + flags := FlagLengthIncluded + var dataToSend []byte + if len(data) > maxChunkSize { + fmt.Printf("Data needs to be chunked: %d\n", len(data)) + flags += FlagMoreFragments + dataToSend = data[:maxChunkSize] + remainingData := data[maxChunkSize:] + // Chunk remaining data into correct chunks and add them to the list + st.RemainingChunks = append(st.RemainingChunks, slices.Collect(slices.Chunk(remainingData, maxChunkSize))...) + } else { + dataToSend = data + } + return &Payload{ + Flags: flags, + Length: uint32(len(data) + 5), + Data: dataToSend, + }, st +} + +func (p *Payload) sendNextChunk(st State) (*Payload, State) { + fmt.Printf("Sending next chunk\n") + nextChunk := st.RemainingChunks[0] + st.RemainingChunks = st.RemainingChunks[1:] + flags := FlagLengthIncluded + if st.HasMore() { + fmt.Printf("More chunks left: %d\n", len(st.RemainingChunks)) + flags += FlagMoreFragments + } + fmt.Printf("Reporting size: %d\n", uint32((len(st.RemainingChunks)*maxChunkSize)+5)) + return &Payload{ + Flags: flags, + Length: uint32((len(st.RemainingChunks) * maxChunkSize) + 5), + Data: nextChunk, + }, st +} diff --git a/internal/outpost/radius/eap/tls/state.go b/internal/outpost/radius/eap/tls/state.go new file mode 100644 index 0000000000..61f3808e94 --- /dev/null +++ b/internal/outpost/radius/eap/tls/state.go @@ -0,0 +1,19 @@ +package tls + +import "crypto/tls" + +type State struct { + HasStarted bool + RemainingChunks [][]byte + TLS *tls.Conn +} + +func NewState() State { + return State{ + RemainingChunks: make([][]byte, 0), + } +} + +func (s State) HasMore() bool { + return len(s.RemainingChunks) > 0 +} diff --git a/internal/outpost/radius/handle_access_request.go b/internal/outpost/radius/handle_access_request.go index 308279cb4b..df3b7a1009 100644 --- a/internal/outpost/radius/handle_access_request.go +++ b/internal/outpost/radius/handle_access_request.go @@ -6,12 +6,25 @@ import ( "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" "goauthentik.io/internal/outpost/flow" + "goauthentik.io/internal/outpost/radius/eap" "goauthentik.io/internal/outpost/radius/metrics" "layeh.com/radius" "layeh.com/radius/rfc2865" + "layeh.com/radius/rfc2869" ) func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusRequest) { + eap := rfc2869.EAPMessage_Get(r.Packet) + if len(eap) > 0 { + rs.log.Trace("EAP request") + rs.Handle_AccessRequest_EAP(w, r) + } else { + rs.log.Trace("PAP request") + rs.Handle_AccessRequest_PAP(w, r) + } +} + +func (rs *RadiusServer) Handle_AccessRequest_PAP(w radius.ResponseWriter, r *RadiusRequest) { username := rfc2865.UserName_GetString(r.Packet) fe := flow.NewFlowExecutor(r.Context(), r.pi.flowSlug, r.pi.s.ac.Client.GetConfig(), log.Fields{ @@ -87,3 +100,29 @@ func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusR res.Add(attr.Type, attr.Attribute) } } + +func (rs *RadiusServer) Handle_AccessRequest_EAP(w radius.ResponseWriter, r *RadiusRequest) { + er := rfc2869.EAPMessage_Get(r.Packet) + ep, err := eap.Decode(er) + if err != nil { + rs.log.WithError(err).Warning("failed to parse EAP packet") + return + } + ep.Handle(r.pi, w, r.Packet) +} + +// ----------- + +func (pi *ProviderInstance) GetEAPState(key string) *eap.State { + return pi.eapState[key] +} + +func (pi *ProviderInstance) SetEAPState(key string, state *eap.State) { + pi.eapState[key] = state +} + +func (pi *ProviderInstance) GetEAPSettings() eap.Settings { + return eap.Settings{ + ChallengesToOffer: []eap.Type{eap.TypeTLS}, + } +} diff --git a/internal/outpost/radius/radius.go b/internal/outpost/radius/radius.go index 491ebff1a9..e01c237c1f 100644 --- a/internal/outpost/radius/radius.go +++ b/internal/outpost/radius/radius.go @@ -9,6 +9,7 @@ import ( log "github.com/sirupsen/logrus" "goauthentik.io/internal/config" "goauthentik.io/internal/outpost/ak" + "goauthentik.io/internal/outpost/radius/eap" "goauthentik.io/internal/outpost/radius/metrics" "layeh.com/radius" @@ -24,6 +25,7 @@ type ProviderInstance struct { providerId int32 s *RadiusServer log *log.Entry + eapState map[string]*eap.State } type RadiusServer struct { diff --git a/notes.md b/notes.md new file mode 100644 index 0000000000..a220c60f0e --- /dev/null +++ b/notes.md @@ -0,0 +1,4 @@ + +eapol_test -s foo -a 192.168.68.1 -c config + +sudo tcpdump -i bridge100 port 1812 -w eap.pcap