├── src ├── go.mod ├── go.sum ├── ip.go ├── main.go ├── Dockerfile └── ipx-handler.go ├── README.md ├── .gitignore ├── LICENSE.md └── .github └── workflows └── build.yml /src/go.mod: -------------------------------------------------------------------------------- 1 | module jsdos/ipx/server 2 | 3 | go 1.20 4 | 5 | require github.com/gorilla/websocket v1.5.0 6 | -------------------------------------------------------------------------------- /src/go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 2 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DOSBox / DOSBox-X ipx server for js-dos 2 | 3 | [![Linux](https://github.com/caiiiycuk/dosbox-ipx-server/actions/workflows/build.yml/badge.svg)](https://github.com/caiiiycuk/dosbox-ipx-server/actions/workflows/build.yml) 4 | 5 | js-dos implementation of dosbox/dosbox-x ipx protocol. Can be used to connect 6 | multipe js-dos instances in one ipx network. 7 | 8 | [Documentation](https://js-dos.com/networking.html) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # binaries 18 | dist 19 | src/server 20 | etc/host.cert 21 | etc/host.key -------------------------------------------------------------------------------- /src/ip.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/binary" 5 | "net" 6 | "strconv" 7 | ) 8 | 9 | func (transport *IPXTransport) setAddress(address string) { 10 | host, portStr, _ := net.SplitHostPort(address) 11 | ip := net.ParseIP(host) 12 | 13 | if len(ip) == 16 { 14 | transport.Host = binary.BigEndian.Uint32(ip[12:16]) 15 | } else { 16 | transport.Host = binary.BigEndian.Uint32(ip) 17 | } 18 | 19 | port, _ := strconv.Atoi(portStr) 20 | transport.Port = uint16(port) 21 | } 22 | 23 | func (transport *IPXTransport) Address() string { 24 | ip := make(net.IP, 4) 25 | binary.BigEndian.PutUint32(ip, transport.Host) 26 | return net.JoinHostPort(ip.String(), strconv.Itoa(int(transport.Port))) 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | dosbox-ipx-server 2 | 3 | Copyright (C) 2024 @caiiiycuk 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or (at 8 | your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, but 11 | WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program; if not, write to the Free Software 17 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 18 | USA. 19 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: linux 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | tags: 10 | - "v*.*.*" 11 | pull_request: 12 | branches: [ main ] 13 | 14 | jobs: 15 | 16 | build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@v4 23 | with: 24 | go-version: '1.20' 25 | 26 | - name: Build 27 | working-directory: ./src 28 | run: go build 29 | 30 | - name: Upload a Build Artifact 31 | uses: actions/upload-artifact@v4.3.6 32 | with: 33 | path: "./src/server" 34 | 35 | - name: Release 36 | uses: softprops/action-gh-release@v2 37 | if: startsWith(github.ref, 'refs/tags/') 38 | with: 39 | name: ${{ github.ref_name }} 40 | files: | 41 | ${{github.workspace}}/src/server 42 | 43 | -------------------------------------------------------------------------------- /src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/gorilla/websocket" 10 | ) 11 | 12 | const port = "1900" 13 | 14 | var upgrader = websocket.Upgrader{ 15 | Subprotocols: []string{"binary"}, 16 | CheckOrigin: func(r *http.Request) bool { 17 | return true 18 | }, 19 | } 20 | 21 | var ipxHandler = &IpxHandler{ 22 | serverAddress: "127.0.0.1:" + port, 23 | } 24 | 25 | func getRoom(r *http.Request) string { 26 | parts := strings.Split(r.URL.Path, "/") 27 | if len(parts) < 3 { 28 | return "" 29 | } 30 | if parts[1] != "ipx" { 31 | return "" 32 | } 33 | return parts[2] 34 | } 35 | 36 | func ipxWebSocket(w http.ResponseWriter, r *http.Request) { 37 | room := getRoom(r) 38 | if len(room) == 0 { 39 | return 40 | } 41 | 42 | conn, err := upgrader.Upgrade(w, r, nil) 43 | if err != nil { 44 | return 45 | } 46 | 47 | ipxHandler.OnConnect(conn, room) 48 | for { 49 | _, data, err := conn.ReadMessage() 50 | if err != nil { 51 | break 52 | } 53 | 54 | ipxHandler.OnMessage(conn, room, data) 55 | } 56 | 57 | ipxHandler.OnClose(conn, room) 58 | conn.Close() 59 | } 60 | 61 | var cert string 62 | var key string 63 | 64 | func main() { 65 | flag.StringVar(&cert, "c", "", ".cert file") 66 | flag.StringVar(&key, "k", "", ".key file") 67 | flag.Parse() 68 | http.HandleFunc("/ipx/", ipxWebSocket) 69 | if len(cert) == 0 || len(key) == 0 { 70 | log.Println(".cert or .key file is not provided, disabling TLS") 71 | if err := http.ListenAndServe(":"+port, nil); err != nil { 72 | log.Fatal(err) 73 | } 74 | } else if err := http.ListenAndServeTLS(":"+port, cert, key, nil); err != nil { 75 | log.Fatal(err) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: THIS DOCKERFILE IS GENERATED VIA "apply-templates.sh" 3 | # 4 | # PLEASE DO NOT EDIT IT DIRECTLY. 5 | # 6 | 7 | FROM alpine:3.14 8 | 9 | RUN apk add --no-cache ca-certificates 10 | 11 | # set up nsswitch.conf for Go's "netgo" implementation 12 | # - https://github.com/golang/go/blob/go1.9.1/src/net/conf.go#L194-L275 13 | # - docker run --rm debian:stretch grep '^hosts:' /etc/nsswitch.conf 14 | RUN [ ! -e /etc/nsswitch.conf ] && echo 'hosts: files dns' > /etc/nsswitch.conf 15 | 16 | ENV PATH /usr/local/go/bin:$PATH 17 | 18 | ENV GOLANG_VERSION 1.17.1 19 | 20 | RUN set -eux; \ 21 | apk add --no-cache --virtual .fetch-deps gnupg; \ 22 | arch="$(apk --print-arch)"; \ 23 | url=; \ 24 | case "$arch" in \ 25 | 'x86_64') \ 26 | export GOARCH='amd64' GOOS='linux'; \ 27 | ;; \ 28 | 'armhf') \ 29 | export GOARCH='arm' GOARM='6' GOOS='linux'; \ 30 | ;; \ 31 | 'armv7') \ 32 | export GOARCH='arm' GOARM='7' GOOS='linux'; \ 33 | ;; \ 34 | 'aarch64') \ 35 | export GOARCH='arm64' GOOS='linux'; \ 36 | ;; \ 37 | 'x86') \ 38 | export GO386='softfloat' GOARCH='386' GOOS='linux'; \ 39 | ;; \ 40 | 'ppc64le') \ 41 | export GOARCH='ppc64le' GOOS='linux'; \ 42 | ;; \ 43 | 's390x') \ 44 | export GOARCH='s390x' GOOS='linux'; \ 45 | ;; \ 46 | *) echo >&2 "error: unsupported architecture '$arch' (likely packaging update needed)"; exit 1 ;; \ 47 | esac; \ 48 | build=; \ 49 | if [ -z "$url" ]; then \ 50 | # https://github.com/golang/go/issues/38536#issuecomment-616897960 51 | build=1; \ 52 | url='https://dl.google.com/go/go1.17.1.src.tar.gz'; \ 53 | sha256='49dc08339770acd5613312db8c141eaf61779995577b89d93b541ef83067e5b1'; \ 54 | # the precompiled binaries published by Go upstream are not compatible with Alpine, so we always build from source here 😅 55 | fi; \ 56 | \ 57 | wget -O go.tgz.asc "$url.asc"; \ 58 | wget -O go.tgz "$url"; \ 59 | echo "$sha256 *go.tgz" | sha256sum -c -; \ 60 | \ 61 | # https://github.com/golang/go/issues/14739#issuecomment-324767697 62 | GNUPGHOME="$(mktemp -d)"; export GNUPGHOME; \ 63 | # https://www.google.com/linuxrepositories/ 64 | gpg --batch --keyserver keyserver.ubuntu.com --recv-keys 'EB4C 1BFD 4F04 2F6D DDCC EC91 7721 F63B D38B 4796'; \ 65 | gpg --batch --verify go.tgz.asc go.tgz; \ 66 | gpgconf --kill all; \ 67 | rm -rf "$GNUPGHOME" go.tgz.asc; \ 68 | \ 69 | tar -C /usr/local -xzf go.tgz; \ 70 | rm go.tgz; \ 71 | \ 72 | if [ -n "$build" ]; then \ 73 | apk add --no-cache --virtual .build-deps \ 74 | bash \ 75 | gcc \ 76 | go \ 77 | musl-dev \ 78 | ; \ 79 | \ 80 | ( \ 81 | cd /usr/local/go/src; \ 82 | # set GOROOT_BOOTSTRAP + GOHOST* such that we can build Go successfully 83 | export GOROOT_BOOTSTRAP="$(go env GOROOT)" GOHOSTOS="$GOOS" GOHOSTARCH="$GOARCH"; \ 84 | ./make.bash; \ 85 | ); \ 86 | \ 87 | apk del --no-network .build-deps; \ 88 | \ 89 | # pre-compile the standard library, just like the official binary release tarballs do 90 | go install std; \ 91 | # go install: -race is only supported on linux/amd64, linux/ppc64le, linux/arm64, freebsd/amd64, netbsd/amd64, darwin/amd64 and windows/amd64 92 | # go install -race std; \ 93 | \ 94 | # remove a few intermediate / bootstrapping files the official binary release tarballs do not contain 95 | rm -rf \ 96 | /usr/local/go/pkg/*/cmd \ 97 | /usr/local/go/pkg/bootstrap \ 98 | /usr/local/go/pkg/obj \ 99 | /usr/local/go/pkg/tool/*/api \ 100 | /usr/local/go/pkg/tool/*/go_bootstrap \ 101 | /usr/local/go/src/cmd/dist/dist \ 102 | ; \ 103 | fi; \ 104 | \ 105 | apk del --no-network .fetch-deps; \ 106 | \ 107 | go version 108 | 109 | ENV GOPATH /go 110 | ENV PATH $GOPATH/bin:$PATH 111 | RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH" 112 | 113 | RUN mkdir /ipx-server 114 | WORKDIR /ipx-server 115 | 116 | COPY . . 117 | 118 | RUN go mod tidy 119 | RUN go build . -------------------------------------------------------------------------------- /src/ipx-handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "sync" 7 | 8 | "github.com/gorilla/websocket" 9 | ) 10 | 11 | type IpxHandler struct { 12 | rooms sync.Map 13 | serverAddress string 14 | } 15 | 16 | type IpxRoom struct { 17 | clients *sync.Map 18 | } 19 | 20 | func (handler *IpxHandler) OnConnect(conn *websocket.Conn, room string) { 21 | address := conn.RemoteAddr().String() 22 | ipxRoom, _ := handler.rooms.LoadOrStore(room, &IpxRoom{ 23 | clients: &sync.Map{}, 24 | }) 25 | clients := ipxRoom.(*IpxRoom).clients 26 | prev, loaded := clients.Swap(address, conn) 27 | if loaded { 28 | prev.(*websocket.Conn).Close() 29 | } 30 | } 31 | 32 | func (handler *IpxHandler) OnMessage(conn *websocket.Conn, room string, data []byte) { 33 | header := IPXHeader{} 34 | header.fromBytes(data) 35 | 36 | if header.Dest.Socket == 0x2 && header.Dest.Host == 0x0 { 37 | // registration 38 | header.CheckSum = 0xffff 39 | header.Length = 30 40 | header.TransControl = 0 41 | header.PType = 0 42 | 43 | header.Dest.Network = 0 44 | header.Dest.setAddress(conn.RemoteAddr().String()) 45 | header.Dest.Socket = 0x2 46 | 47 | header.Src.Network = 1 48 | header.Src.setAddress(handler.serverAddress) 49 | header.Src.Socket = 0x2 50 | 51 | conn.WriteMessage(websocket.BinaryMessage, header.toBytes()) 52 | } else { 53 | ipxRoom, ok := handler.rooms.Load(room) 54 | if !ok { 55 | return 56 | } 57 | clients := ipxRoom.(*IpxRoom).clients 58 | 59 | if header.Dest.Host == 0xffffffff { 60 | // broadcast 61 | clients.Range(func(address, dest interface{}) bool { 62 | if conn != dest { 63 | dest.(*websocket.Conn).WriteMessage(websocket.BinaryMessage, data) 64 | } 65 | return true 66 | }) 67 | } else { 68 | dest, ok := clients.Load(header.Dest.Address()) 69 | if ok { 70 | dest.(*websocket.Conn).WriteMessage(websocket.BinaryMessage, data) 71 | } 72 | } 73 | } 74 | } 75 | 76 | func (handler *IpxHandler) OnClose(conn *websocket.Conn, room string) { 77 | address := conn.RemoteAddr().String() 78 | ipxRoom, ok := handler.rooms.Load(room) 79 | if ok { 80 | clients := ipxRoom.(*IpxRoom).clients 81 | clients.Delete(address) 82 | empty := true 83 | clients.Range(func(_, _ interface{}) bool { 84 | empty = false 85 | return false 86 | }) 87 | // @caiiiycuk: we can accidentialy delete non empty room, 88 | if empty { 89 | handler.rooms.Delete(room) 90 | } 91 | } 92 | } 93 | 94 | type IPXTransport struct { 95 | Network uint32 96 | Host uint32 97 | Port uint16 98 | Socket uint16 99 | } 100 | 101 | type IPXHeader struct { 102 | CheckSum uint16 103 | Length uint16 104 | TransControl uint8 105 | PType uint8 106 | 107 | Dest IPXTransport 108 | Src IPXTransport 109 | } 110 | 111 | func (header *IPXHeader) toBytes() []byte { 112 | var payload bytes.Buffer 113 | binary.Write(&payload, binary.BigEndian, header.CheckSum) 114 | binary.Write(&payload, binary.BigEndian, header.Length) 115 | binary.Write(&payload, binary.BigEndian, header.TransControl) 116 | binary.Write(&payload, binary.BigEndian, header.PType) 117 | 118 | binary.Write(&payload, binary.BigEndian, header.Dest.Network) 119 | binary.Write(&payload, binary.BigEndian, header.Dest.Host) 120 | binary.Write(&payload, binary.BigEndian, header.Dest.Port) 121 | binary.Write(&payload, binary.BigEndian, header.Dest.Socket) 122 | 123 | binary.Write(&payload, binary.BigEndian, header.Src.Network) 124 | binary.Write(&payload, binary.BigEndian, header.Src.Host) 125 | binary.Write(&payload, binary.BigEndian, header.Src.Port) 126 | binary.Write(&payload, binary.BigEndian, header.Src.Socket) 127 | return payload.Bytes() 128 | } 129 | 130 | func (header *IPXHeader) fromBytes(data []byte) { 131 | payload := bytes.NewReader(data) 132 | binary.Read(payload, binary.BigEndian, &header.CheckSum) 133 | binary.Read(payload, binary.BigEndian, &header.Length) 134 | binary.Read(payload, binary.BigEndian, &header.TransControl) 135 | binary.Read(payload, binary.BigEndian, &header.PType) 136 | 137 | binary.Read(payload, binary.BigEndian, &header.Dest.Network) 138 | binary.Read(payload, binary.BigEndian, &header.Dest.Host) 139 | binary.Read(payload, binary.BigEndian, &header.Dest.Port) 140 | binary.Read(payload, binary.BigEndian, &header.Dest.Socket) 141 | 142 | binary.Read(payload, binary.BigEndian, &header.Src.Network) 143 | binary.Read(payload, binary.BigEndian, &header.Src.Host) 144 | binary.Read(payload, binary.BigEndian, &header.Src.Port) 145 | binary.Read(payload, binary.BigEndian, &header.Src.Socket) 146 | } 147 | --------------------------------------------------------------------------------