实现
为了实现在飞牛上敲写代码,我将docker容器加上了iframe加载,然后变成了用systemctl来挂载code-server服务,最后想用飞牛统一网关,但是code-server默认运行在/根目录,而飞牛的统一网关要加/app/xxx的前缀,解决办法就是加中间层,最开始用的是node.js写了个中间层,结果是websocket连接不上,后面用go来手写很意外的成功了,然后后想手写优化,优化了一下就出来之前的问题,如下图所示:

后面,反复修改终于确认了问题根源,之前的nodejs版本就是这个问题解决不了,在代理给 Unix Socket 时,请求头里的 Host 可能变成了 unix 或者与 Origin 的协议不匹配。code-server 看到 Origin 和 Host 不一致,触发了安全防御机制,直接掐断了连接(导致 1006 错误)。而手动拼接字符串粗糙代码,手动复制了 Sec-WebSocket-* 和 Cookie,完全漏掉了浏览器的 Origin 请求头,code-server 发现没有 Origin 头,以为是某种非浏览器的本地客户端发起的请求,直接跳过了安全校验,所以放行了就连接成功了,所以修复起来就非常简单:要么让 Host 和 Origin 完美匹配,要么直接把 Origin 头删掉来绕过校验,这下终于code-server用上了统一网关了,fpk下载地址:https://github.com/ctllo-bit/coder
go中间层
这个golang转发层是最核心的,最初以为只要改写路径就可以了,结果是websocket连不上,才知道websocket也要转发,转发了还是1006 的错误,才又知道有个CORS的跨域与主机头安全校验的问题,最终都解决了,就写出了下面的http/websocket统一代理层,代码如下所示:
package main
import (
"context"
"flag"
"log"
"net"
"net/http"
"net/http/httputil"
"os"
"os/signal"
"strings"
"syscall"
"time"
)
var (
codeServerSocket = flag.String("socket", "/var/apps/coder/var/code-server.sock", "upstream code-server unix socket")
proxySocket = flag.String("proxy-socket", "/var/apps/coder/target/coder-proxy.sock", "this proxy's own unix socket")
prefix = flag.String("prefix", "/app/coder", "URL prefix to strip before forwarding")
)
func main() {
flag.Parse()
// 不再使用 httputil.NewSingleHostReverseProxy,直接初始化 ReverseProxy 结构体
backend := &httputil.ReverseProxy{
// ====== 使用 Rewrite 替代 Director ======
Rewrite: func(pr *httputil.ProxyRequest) {
pr.SetXForwarded() // 自动处理 X-Forwarded-* 头
// 处理路径前缀剥离
path := pr.In.URL.Path
if strings.HasPrefix(path, *prefix) {
path = strings.TrimPrefix(path, *prefix)
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
}
// 设置目标 Scheme 和 Host
pr.Out.URL.Scheme = "http"
pr.Out.URL.Host = "unix"
pr.Out.URL.Path = path
// 将外部真实的 Host 传递给后端
pr.Out.Host = pr.In.Host
// 删除 Origin 头,绕过 code-server 严格的同源检测
pr.Out.Header.Del("Origin")
},
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, "unix", *codeServerSocket)
},
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
},
ModifyResponse: func(r *http.Response) error {
if loc := r.Header.Get("Location"); strings.HasPrefix(loc, "/") && !strings.HasPrefix(loc, *prefix) {
r.Header.Set("Location", *prefix+loc)
}
return nil
},
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("proxy error: %s %s -> %v", r.Method, r.URL.Path, err)
if strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "no such file") {
http.Error(w, "code-server is not running or the socket does not exist", http.StatusServiceUnavailable)
} else {
http.Error(w, "Bad Gateway", http.StatusBadGateway)
}
},
}
if err := os.RemoveAll(*proxySocket); err != nil {
log.Fatalf("failed to remove old proxy socket: %v", err)
}
listener, err := net.Listen("unix", *proxySocket)
if err != nil {
log.Fatalf("failed to listen on proxy socket %s: %v", *proxySocket, err)
}
if err := os.Chmod(*proxySocket, 0666); err != nil {
log.Printf("warning: failed to set socket permissions: %v", err)
}
// 优雅停机逻辑
server := &http.Server{
Handler: backend, // 直接将 backend 作为 Server 的 Handler
}
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
log.Println("shutting down proxy...")
// 给活动连接 5 秒钟时间完成关闭
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Printf("proxy shutdown error: %v", err)
}
}()
log.Printf("coder proxy listening on %s", *proxySocket)
log.Printf("upstream: unix://%s prefix=%s", *codeServerSocket, *prefix)
// 使用 server.Serve 替代 http.Serve
if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
os.RemoveAll(*proxySocket)
}
go程序编译:
GOOS=linux GOARCH=amd64 go build -o coder-proxy-linux-amd64 main.go
GOOS=linux GOARCH=arm64 go build -o coder-proxy-linux-arm64 main.go