I am attempting to set up websockets with TLS within Google Kubernetes Engine and Istio.
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: example-back-end
spec:
hosts:
- "api-dev.example.dev"
gateways:
- istio-system/example-gateway
http:
- match:
- uri:
prefix: /worker
route:
- destination:
host: worker
port:
number: 5001
- match:
- uri:
prefix: /
route:
- destination:
host: back-end
port:
number: 5000
- match:
- uri:
prefix: /ws
route:
- destination:
host: service-websocket
port:
number: 8080
websocketUpgrade: trueI've mounted the tls cert and key into my websocket service container. (The same one I'm using for the api.example.com).
apiVersion: v1
kind: Service
metadata:
name: service-websocket
spec:
selector:
app: service-websocket
ports:
- port: 8080
targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: service-websocket
spec:
selector:
matchLabels:
app: service-websocket
template:
metadata:
labels:
app: service-websocket
spec:
volumes:
- name: example-certificate
secret:
secretName: example-certificate
containers:
- name: service-websocket
image: gcr.io/example-project/service-websocket:latest
resources:
limits:
memory: "128Mi"
cpu: "500m"
ports:
- containerPort: 8080
volumeMounts:
- name: example-certificate
mountPath: /var/secrets/tlsThis is the websocket server which uses the ws node package.
// web server
const https = require("https");
const config = require("./config");
const fs = require("fs");
const server = https.createServer({
cert: fs.readFileSync(config.TLS_CERT),
key: fs.readFileSync(config.TLS_KEY)
});
// websocket
const WebSocket = require("ws");
const url = require("url");
const wss = new WebSocket.Server({ noServer: true });
wss.on("connection", function connection(ws, req) {
const parameters = url.parse(req.url, true);
ws.on("message", function incoming(message) {
wss.clients.forEach(client => {
const msg = {
msg: "hello world from server"
};
client.send(JSON.stringify(msg));
});
});
const msg = {
msg: "something"
};
ws.send(JSON.stringify(msg));
});
wss.on("error", () => console.log("error"));
server.on("upgrade", function upgrade(request, socket, head) {
const pathname = url.parse(request.url).pathname;
if (pathname === "/ws") {
wss.handleUpgrade(request, socket, head, function done(ws) {
wss.emit("connection", ws, request);
});
} else {
socket.destroy();
}
});
server.listen(8080);From the frontend, I'm initializing Websocket:
const ws = new WebSocket(`wss://api.example.com/ws`);However, I am getting the error:
WebSocket connection to 'wss://api.example.com/ws' failed: Error during WebSocket handshake: Unexpected response code: 404Everything seems to work fine locally within my docker-compose setup. But can't seem to figure out how to make this work on GKE + Istio.
UPDATE 1/15/20
I changed the virtual service route order. Previously, /ws was after /. But now I am getting a 503 error from the frontend.
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: example-back-end
spec:
hosts:
- "api-dev.example.dev"
gateways:
- istio-system/example-gateway
http:
- match:
- uri:
prefix: /ws
route:
- destination:
host: service-websocket
port:
number: 443
websocketUpgrade: true
- match:
- uri:
prefix: /worker
route:
- destination:
host: worker
port:
number: 5001
- match:
- uri:
prefix: /
route:
- destination:
host: back-end
port:
number: 5000The main issue was that TLS termination was happening at the gateway instead of the service layer. Istio has a tls passthrough mode which allows end to end encryption of the websocket connection. The solution involves several parts.
ws.dev.example.dev to dev.example.dev. This is key as URI prefix matching isn't supported, hence why the /ws didn't work before.ws.dev.example.dev is used when connecting to the websocket service.gateway.yaml
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: example-gateway
namespace: istio-system
labels:
app: ingressgateway
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 443
protocol: HTTPS
name: https-ws
tls:
mode: PASSTHROUGH
hosts:
- "ws.dev.example.dev"service-websocket-virtual-service.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: service-websocket
spec:
hosts:
- "ws.dev.example.dev"
gateways:
- istio-system/example-gateway
tls:
- match:
- port: 443
sni_hosts:
- "ws.dev.example.dev"
route:
- destination:
host: service-websocket
port:
number: 443service-websocket.yaml
apiVersion: v1
kind: Service
metadata:
name: service-websocket
spec:
selector:
app: service-websocket
ports:
- port: 443
targetPort: 443
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: service-websocket
spec:
selector:
matchLabels:
app: service-websocket
template:
metadata:
labels:
app: service-websocket
spec:
volumes:
- name: example-dev-certificate
secret:
secretName: example-dev-certificate
containers:
- name: service-websocket
image: gcr.io/example-dev/service-websocket:latest
resources:
limits:
memory: "128Mi"
cpu: "500m"
ports:
- containerPort: 443
volumeMounts:
- name: example-dev-certificate
mountPath: /var/secrets/tlsservice-websocket node server
const https = require("https");
const config = require("./config");
const fs = require("fs");
const server = https.createServer({
cert: fs.readFileSync(config.TLS_CERT),
key: fs.readFileSync(config.TLS_KEY)
});
// websocket
const WebSocket = require("ws");
const url = require("url");
const wss = new WebSocket.Server({ noServer: true });
wss.on("connection", function connection(ws, req) {
const parameters = url.parse(req.url, true);
ws.ideaRoom = { id: parseInt(parameters.query.ideaId) };
console.log("ws.ideaRoom", ws.ideaRoom);
ws.on("message", function incoming(message) {
console.log("received: %s", message);
console.log("wss.clients", wss.clients);
wss.clients.forEach(client => {
const msg = {
msg: "hello world from server " + ws.ideaRoom.id
};
client.send(JSON.stringify(msg));
});
});
const msg = {
msg: "something"
};
ws.send(JSON.stringify(msg));
});
wss.on("error", () => console.log("error"));
server.on("upgrade", function upgrade(request, socket, head) {
const pathname = url.parse(request.url).pathname;
console.log("pathname", pathname);
if (pathname === "/") {
wss.handleUpgrade(request, socket, head, function done(ws) {
wss.emit("connection", ws, request);
});
} else {
socket.destroy();
}
});
server.listen(443);frontend code
const ws = new WebSocket(`wss://ws.dev.example.dev:443?ideaId=${idea.id}`);
ws.onopen = () => {
ws.send("hello world from client: " + idea.id);
};
ws.onerror = error => {
console.error(error);
};
ws.onmessage = e => {
console.log('e.data', e.data);
const idea_comment = JSON.parse(e.data);
console.log("idea_comment", idea_comment);
};