Skip to content

Commit d5ca0da

Browse files
committed
Fix vehicle identity spoofing and bind monitoring servers to localhost
1 parent 031553a commit d5ca0da

4 files changed

Lines changed: 162 additions & 20 deletions

File tree

metrics/metrics.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,18 @@ type MonitoringConfig struct {
1818
// PrometheusMetricsPort port to run prometheus on
1919
PrometheusMetricsPort int `json:"prometheus_metrics_port,omitempty"`
2020

21+
// PrometheusMetricsHost is the address to bind the prometheus metrics server to (default "127.0.0.1")
22+
PrometheusMetricsHost string `json:"prometheus_metrics_host,omitempty"`
23+
2124
// Statsd metrics if you are not using prometheus
2225
Statsd *StatsdConfig `json:"statsd,omitempty"`
2326

2427
// ProfilerPort if non-zero enable http profiler on this port
2528
ProfilerPort int `json:"profiler_port,omitempty"`
2629

30+
// ProfilerHost is the address to bind the profiler to (default "127.0.0.1")
31+
ProfilerHost string `json:"profiler_host,omitempty"`
32+
2733
// ProfilingPath is the variable that enable deep profiling is set
2834
ProfilingPath string `json:"profiling_path,omitempty"`
2935

server/monitoring/metrics_server.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import (
66
"sync"
77
"time"
88

9-
// This registers the profiler on the default mux which we will use for monitoring port.
10-
_ "net/http/pprof"
9+
"net/http/pprof"
1110

1211
"github.com/prometheus/client_golang/prometheus/promhttp"
1312

@@ -37,17 +36,32 @@ func StartServerMetrics(config *config.Config, logger *logrus.Logger, registry *
3736
promMux := http.NewServeMux()
3837
promMux.Handle("/metrics", promhttp.Handler())
3938
go func() {
40-
if err := http.ListenAndServe(fmt.Sprintf(":%d", config.Monitoring.PrometheusMetricsPort), promMux); err != nil {
39+
metricsHost := config.Monitoring.PrometheusMetricsHost
40+
if metricsHost == "" {
41+
metricsHost = "127.0.0.1"
42+
}
43+
if err := http.ListenAndServe(fmt.Sprintf("%s:%d", metricsHost, config.Monitoring.PrometheusMetricsPort), promMux); err != nil {
4144
logger.ErrorLog("metrics_server_err", err, nil)
4245
}
4346
}()
4447
}
4548

4649
if config.Monitoring.ProfilerPort > 0 {
4750
go func() {
48-
StartProfilerServer(config, http.DefaultServeMux, logger)
51+
profilerMux := http.NewServeMux()
52+
profilerMux.HandleFunc("/debug/pprof/", pprof.Index)
53+
profilerMux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
54+
profilerMux.HandleFunc("/debug/pprof/profile", pprof.Profile)
55+
profilerMux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
56+
profilerMux.HandleFunc("/debug/pprof/trace", pprof.Trace)
57+
58+
StartProfilerServer(config, profilerMux, logger)
4959

50-
if err := http.ListenAndServe(fmt.Sprintf(":%d", config.Monitoring.ProfilerPort), nil); err != nil {
60+
profilerHost := config.Monitoring.ProfilerHost
61+
if profilerHost == "" {
62+
profilerHost = "127.0.0.1"
63+
}
64+
if err := http.ListenAndServe(fmt.Sprintf("%s:%d", profilerHost, config.Monitoring.ProfilerPort), profilerMux); err != nil {
5165
logger.ErrorLog("profiler_listen_error", err, nil)
5266
}
5367
}()

server/streaming/server.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ func (s *Server) ServeBinaryWs(config *config.Config) func(w http.ResponseWriter
132132
requestIdentity, err := extractIdentityFromConnection(r)
133133
if err != nil {
134134
s.logger.ErrorLog("extract_sender_id_err", err, nil)
135+
_ = ws.Close()
136+
return
135137
}
136138

137139
binarySerializer := telemetry.NewBinarySerializer(requestIdentity, s.DispatchRules, s.logger)
@@ -236,12 +238,13 @@ func extractIdentityFromConnection(r *http.Request) (*telemetry.RequestIdentity,
236238
}
237239

238240
func extractCertFromHeaders(r *http.Request) (*x509.Certificate, error) {
239-
nbCerts := len(r.TLS.PeerCertificates)
240-
if nbCerts == 0 {
241-
return nil, fmt.Errorf("missing_certificate_error")
241+
if r.TLS == nil {
242+
return nil, fmt.Errorf("missing_tls_state")
242243
}
243-
244-
return r.TLS.PeerCertificates[nbCerts-1], nil
244+
if len(r.TLS.VerifiedChains) == 0 || len(r.TLS.VerifiedChains[0]) == 0 {
245+
return nil, fmt.Errorf("missing_verified_client_certificate")
246+
}
247+
return r.TLS.VerifiedChains[0][0], nil
245248
}
246249

247250
func registerServerMetricsOnce(metricsCollector metrics.MetricCollector) {

server/streaming/server_test.go

Lines changed: 129 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package streaming_test
22

33
import (
4+
"crypto/tls"
5+
"crypto/x509"
6+
"crypto/x509/pkix"
47
"net/http"
58
"net/http/httptest"
69
"net/url"
@@ -13,12 +16,38 @@ import (
1316

1417
"github.com/teslamotors/fleet-telemetry/config"
1518
logrus "github.com/teslamotors/fleet-telemetry/logger"
19+
"github.com/teslamotors/fleet-telemetry/messages"
1620
"github.com/teslamotors/fleet-telemetry/metrics/adapter/noop"
1721
"github.com/teslamotors/fleet-telemetry/server/airbrake"
1822
"github.com/teslamotors/fleet-telemetry/server/streaming"
1923
"github.com/teslamotors/fleet-telemetry/telemetry"
2024
)
2125

26+
// withTLSState wraps a handler to inject a tls.ConnectionState into each request,
27+
// simulating an mTLS connection for testing without actual TLS transport.
28+
func withTLSState(handler http.Handler, tlsState *tls.ConnectionState) http.Handler {
29+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
30+
r.TLS = tlsState
31+
handler.ServeHTTP(w, r)
32+
})
33+
}
34+
35+
func makeCert(commonName, issuerCommonName string) *x509.Certificate {
36+
return &x509.Certificate{
37+
Subject: pkix.Name{CommonName: commonName},
38+
Issuer: pkix.Name{CommonName: issuerCommonName},
39+
}
40+
}
41+
42+
type spyProducer struct {
43+
captured chan *telemetry.Record
44+
}
45+
46+
func (p *spyProducer) Close() error { return nil }
47+
func (p *spyProducer) Produce(entry *telemetry.Record) { p.captured <- entry }
48+
func (p *spyProducer) ProcessReliableAck(_ *telemetry.Record) {}
49+
func (p *spyProducer) ReportError(_ string, _ error, _ logrus.LogInfo) {}
50+
2251
var _ = Describe("Socket handler test", func() {
2352

2453
var producerRules map[string][]telemetry.Producer
@@ -38,8 +67,8 @@ var _ = Describe("Socket handler test", func() {
3867
}
3968
})
4069

41-
It("ServeBinaryWs test", func() {
42-
logger, hook := logrus.NoOpLogger()
70+
It("ServeBinaryWs closes connection when TLS state is missing", func() {
71+
logger, _ := logrus.NoOpLogger()
4372
conf := &config.Config{
4473
RateLimit: &config.RateLimit{
4574
MessageLimit: 1,
@@ -49,29 +78,119 @@ var _ = Describe("Socket handler test", func() {
4978
}
5079

5180
registry := streaming.NewSocketRegistry()
52-
req := httptest.NewRequest("GET", "http://test-server.example.com", nil)
53-
54-
producerRules := make(map[string][]telemetry.Producer)
81+
producerRules = make(map[string][]telemetry.Producer)
5582
_, s, err := streaming.InitServer(conf, airbrake.NewAirbrakeHandler(nil), producerRules, logger, registry)
5683
Expect(err).NotTo(HaveOccurred())
5784

85+
// No TLS state injected, simulates a non-mTLS connection
5886
srv := httptest.NewServer(http.HandlerFunc(s.ServeBinaryWs(conf)))
87+
defer srv.Close()
5988
u, _ := url.Parse(srv.URL)
6089
u.Scheme = "ws"
6190

6291
dialer := &websocket.Dialer{HandshakeTimeout: 1 * time.Second}
63-
conn, _, err := dialer.Dial(u.String(), req.Header)
92+
conn, _, err := dialer.Dial(u.String(), nil)
6493
Expect(err).NotTo(HaveOccurred())
6594

66-
err = conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
95+
// Server should close the connection due to missing TLS state
96+
err = conn.SetReadDeadline(time.Now().Add(1 * time.Second))
6797
Expect(err).NotTo(HaveOccurred())
98+
_, _, err = conn.ReadMessage()
99+
Expect(err).To(HaveOccurred())
100+
_ = conn.Close()
101+
})
68102

69-
err = conn.WriteMessage(websocket.BinaryMessage, []byte(""))
103+
It("ServeBinaryWs closes connection when VerifiedChains is empty", func() {
104+
logger, _ := logrus.NoOpLogger()
105+
conf := &config.Config{
106+
RateLimit: &config.RateLimit{
107+
MessageLimit: 1,
108+
MessageIntervalTimeSecond: 1 * time.Second,
109+
},
110+
MetricCollector: noop.NewCollector(),
111+
}
112+
113+
registry := streaming.NewSocketRegistry()
114+
producerRules = make(map[string][]telemetry.Producer)
115+
_, s, err := streaming.InitServer(conf, airbrake.NewAirbrakeHandler(nil), producerRules, logger, registry)
70116
Expect(err).NotTo(HaveOccurred())
71117

72-
_, _, _ = conn.ReadMessage()
118+
// TLS state with PeerCertificates but no VerifiedChains
119+
tlsState := &tls.ConnectionState{
120+
PeerCertificates: []*x509.Certificate{
121+
makeCert("device-1", "TeslaMotors"),
122+
},
123+
VerifiedChains: nil,
124+
}
125+
srv := httptest.NewServer(withTLSState(http.HandlerFunc(s.ServeBinaryWs(conf)), tlsState))
126+
defer srv.Close()
127+
u, _ := url.Parse(srv.URL)
128+
u.Scheme = "ws"
129+
130+
dialer := &websocket.Dialer{HandshakeTimeout: 1 * time.Second}
131+
conn, _, err := dialer.Dial(u.String(), nil)
132+
Expect(err).NotTo(HaveOccurred())
133+
134+
// Server should close the connection due to empty VerifiedChains
135+
err = conn.SetReadDeadline(time.Now().Add(1 * time.Second))
136+
Expect(err).NotTo(HaveOccurred())
137+
_, _, err = conn.ReadMessage()
138+
Expect(err).To(HaveOccurred())
73139
_ = conn.Close()
140+
})
141+
142+
It("ServeBinaryWs uses verified leaf cert, not last PeerCertificate", func() {
143+
logger, _ := logrus.NoOpLogger()
144+
145+
spy := &spyProducer{captured: make(chan *telemetry.Record, 1)}
146+
147+
conf := &config.Config{
148+
RateLimit: &config.RateLimit{
149+
MessageLimit: 1,
150+
MessageIntervalTimeSecond: 1 * time.Second,
151+
},
152+
MetricCollector: noop.NewCollector(),
153+
}
154+
155+
registry := streaming.NewSocketRegistry()
156+
producerRules = map[string][]telemetry.Producer{"V": {spy}}
157+
_, s, err := streaming.InitServer(conf, airbrake.NewAirbrakeHandler(nil), producerRules, logger, registry)
158+
Expect(err).NotTo(HaveOccurred())
159+
160+
realCert := makeCert("device-1", "TeslaMotors")
161+
spoofedCert := makeCert("SPOOFED-VIN", "TeslaMotors")
162+
163+
tlsState := &tls.ConnectionState{
164+
PeerCertificates: []*x509.Certificate{realCert, spoofedCert},
165+
VerifiedChains: [][]*x509.Certificate{{realCert}},
166+
}
167+
srv := httptest.NewServer(withTLSState(http.HandlerFunc(s.ServeBinaryWs(conf)), tlsState))
168+
defer srv.Close()
169+
u, _ := url.Parse(srv.URL)
170+
u.Scheme = "ws"
171+
172+
dialer := &websocket.Dialer{HandshakeTimeout: 1 * time.Second}
173+
conn, _, err := dialer.Dial(u.String(), nil)
174+
Expect(err).NotTo(HaveOccurred())
175+
defer func() { _ = conn.Close() }()
176+
177+
streamMsg := messages.StreamMessage{
178+
TXID: []byte("test-txid"),
179+
SenderID: []byte("vehicle_device.blah"),
180+
DeviceID: []byte("blah"),
181+
DeviceType: []byte("vehicle_device"),
182+
MessageTopic: []byte("V"),
183+
Payload: []byte{},
184+
}
185+
msgBytes, err := streamMsg.ToBytes()
186+
Expect(err).NotTo(HaveOccurred())
187+
188+
err = conn.WriteMessage(websocket.BinaryMessage, msgBytes)
189+
Expect(err).NotTo(HaveOccurred())
74190

75-
Expect(hook.AllEntries()).To(BeEmpty())
191+
// The record VIN must come from the verified cert ("device-1")
192+
var record *telemetry.Record
193+
Eventually(spy.captured).Should(Receive(&record))
194+
Expect(record.Vin).To(Equal("device-1"))
76195
})
77196
})

0 commit comments

Comments
 (0)