diff --git a/Dockerfile b/Dockerfile index 4e410b8e..53a6cc88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,7 @@ FROM docker.io/library/node:18-alpine AS build_node_modules # Copy Web UI COPY src/ /app/ WORKDIR /app + RUN npm ci --omit=dev &&\ mv node_modules /node_modules @@ -23,6 +24,9 @@ COPY --from=build_node_modules /app /app # than what runs inside of docker. COPY --from=build_node_modules /node_modules /node_modules +# only for china developer +RUN npm config set registry https://registry.npmmirror.com && sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories + RUN \ # Enable this to run `npm run serve` npm i -g nodemon &&\ diff --git a/src/lib/Server.js b/src/lib/Server.js index b6b2f5f4..5aacebb6 100644 --- a/src/lib/Server.js +++ b/src/lib/Server.js @@ -210,6 +210,15 @@ module.exports = class Server { const { address } = await readBody(event); await WireGuard.updateClientAddress({ clientId, address }); return { success: true }; + })) + .put('/api/wireguard/client/:clientId/ipSegSelected', defineEventHandler(async (event) => { + const clientId = getRouterParam(event, 'clientId'); + if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') { + throw createError({ status: 403 }); + } + const { ipSegSelected } = await readBody(event); + await WireGuard.updateClientIpSegSelected({ clientId, ipSegSelected }); + return { success: true }; })); // Static assets diff --git a/src/lib/WireGuard.js b/src/lib/WireGuard.js index 739676fb..5ac0eead 100644 --- a/src/lib/WireGuard.js +++ b/src/lib/WireGuard.js @@ -105,13 +105,19 @@ PostDown = ${WG_POST_DOWN} for (const [clientId, client] of Object.entries(config.clients)) { if (!client.enabled) continue; + let allowedIPs = `${client.address}/32`; + if (client.ipSegSelected && client.ipSegSelected.length > 0) { + const joinedIpSeg = client.ipSegSelected.join(','); + allowedIPs += `,${joinedIpSeg}`; + } + debug(`allowedIPs:${allowedIPs}`); result += ` # Client: ${client.name} (${clientId}) [Peer] PublicKey = ${client.publicKey} ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : '' -}AllowedIPs = ${client.address}/32`; +}AllowedIPs = ${allowedIPs}`; } debug('Config saving...'); @@ -137,6 +143,8 @@ ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : '' name: client.name, enabled: client.enabled, address: client.address, + ipSegSelected: client.ipSegSelected, + ipSegSelecting: client.ipSegSelecting, publicKey: client.publicKey, createdAt: new Date(client.createdAt), updatedAt: new Date(client.updatedAt), @@ -192,9 +200,25 @@ ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : '' return client; } + async getAllCIDR() { + const config = await this.getConfig(); + const clients = await this.getClients(); + let allCIDRs = []; + clients.forEach(client => { + allCIDRs = allCIDRs.concat(client.ipSegSelected); + }); + return allCIDRs.join(','); + } + async getClientConfiguration({ clientId }) { const config = await this.getConfig(); const client = await this.getClient({ clientId }); + let allowedIPs = await this.getAllCIDR(); + if (allowedIPs === "") { + allowedIPs = WG_ALLOWED_IPS; + } else { + allowedIPs = `${WG_ALLOWED_IPS},${allowedIPs}`; + } return ` [Interface] @@ -206,7 +230,7 @@ ${WG_MTU ? `MTU = ${WG_MTU}\n` : ''}\ [Peer] PublicKey = ${config.server.publicKey} ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : '' -}AllowedIPs = ${WG_ALLOWED_IPS} +}AllowedIPs = ${allowedIPs} PersistentKeepalive = ${WG_PERSISTENT_KEEPALIVE} Endpoint = ${WG_HOST}:${WG_PORT}`; } @@ -253,6 +277,8 @@ Endpoint = ${WG_HOST}:${WG_PORT}`; id, name, address, + ipSegSelected: [], + ipSegSelecting: [], privateKey, publicKey, preSharedKey, @@ -319,6 +345,21 @@ Endpoint = ${WG_HOST}:${WG_PORT}`; await this.saveConfig(); } + async updateClientIpSegSelected({ clientId, ipSegSelected }) { + const client = await this.getClient({ clientId }); + + const ipSegRegex = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/; + for (const address of ipSegSelected) { + if (!ipSegRegex.test(address)) { + throw new ServerError(`Invalid Address: ${address}`, 400); + } + } + client.ipSegSelected = ipSegSelected; + client.updatedAt = new Date(); + + await this.saveConfig(); + } + // Shutdown wireguard async Shutdown() { await Util.exec('wg-quick down wg0').catch(() => { }); diff --git a/src/www/css/app.css b/src/www/css/app.css index 1db1aa06..4d1f5061 100644 --- a/src/www/css/app.css +++ b/src/www/css/app.css @@ -590,6 +590,53 @@ video { } } +.multiselect--active:where(.dark, .dark *), + .multiselect__content-wrapper:where(.dark, .dark *), + .multiselect:where(.dark, .dark *), + .multiselect__input:where(.dark, .dark *), + .multiselect__tags:where(.dark, .dark *) { + --tw-bg-opacity: 1; + background-color: rgb(82 82 82 / var(--tw-bg-opacity)); + --tw-text-opacity: 1; + color: rgb(212 212 212 / var(--tw-text-opacity)); +} + +.multiselect--active:where(.dark, .dark *)::-moz-placeholder, .multiselect__content-wrapper:where(.dark, .dark *)::-moz-placeholder, .multiselect:where(.dark, .dark *)::-moz-placeholder, .multiselect__input:where(.dark, .dark *)::-moz-placeholder, .multiselect__tags:where(.dark, .dark *)::-moz-placeholder { + --tw-text-opacity: 1; + color: rgb(163 163 163 / var(--tw-text-opacity)); +} + +.multiselect--active:where(.dark, .dark *)::placeholder, + .multiselect__content-wrapper:where(.dark, .dark *)::placeholder, + .multiselect:where(.dark, .dark *)::placeholder, + .multiselect__input:where(.dark, .dark *)::placeholder, + .multiselect__tags:where(.dark, .dark *)::placeholder { + --tw-text-opacity: 1; + color: rgb(163 163 163 / var(--tw-text-opacity)); +} + +.multiselect__tag { + --tw-bg-opacity: 1; + background-color: rgb(153 27 27 / var(--tw-bg-opacity)); +} + +.multiselect__tag-icon { + --tw-bg-opacity: 1; + background-color: rgb(153 27 27 / var(--tw-bg-opacity)); + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.multiselect__tag-icon:hover { + --tw-bg-opacity: 1; + background-color: rgb(185 28 28 / var(--tw-bg-opacity)); +} + +.multiselect__content-wrapper, + .multiselect__content { + display: none; +} + .sr-only { position: absolute; width: 1px; @@ -622,6 +669,10 @@ video { position: relative; } +.sticky { + position: sticky; +} + .inset-0 { inset: 0px; } @@ -1892,4 +1943,4 @@ video { .peer:checked ~ .peer-checked\:dark\:fill-neutral-400:where(.dark, .dark *) { fill: #a3a3a3; -} +} \ No newline at end of file diff --git a/src/www/css/vue-multiselect.min.css b/src/www/css/vue-multiselect.min.css new file mode 100644 index 00000000..00f94045 --- /dev/null +++ b/src/www/css/vue-multiselect.min.css @@ -0,0 +1 @@ +fieldset[disabled] .multiselect{pointer-events:none}.multiselect__spinner{position:absolute;right:1px;top:1px;width:40px;height:38px;background:#fff;display:block}.multiselect__spinner:after,.multiselect__spinner:before{position:absolute;content:"";top:50%;left:50%;margin:-8px 0 0 -8px;width:16px;height:16px;border-radius:100%;border:2px solid transparent;border-top-color:#41b883;-webkit-box-shadow:0 0 0 1px transparent;box-shadow:0 0 0 1px transparent}.multiselect__spinner:before{-webkit-animation:spinning 2.4s cubic-bezier(.41,.26,.2,.62);animation:spinning 2.4s cubic-bezier(.41,.26,.2,.62);-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.multiselect__spinner:after{-webkit-animation:spinning 2.4s cubic-bezier(.51,.09,.21,.8);animation:spinning 2.4s cubic-bezier(.51,.09,.21,.8);-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.multiselect__loading-enter-active,.multiselect__loading-leave-active{-webkit-transition:opacity .4s ease-in-out;transition:opacity .4s ease-in-out;opacity:1}.multiselect__loading-enter,.multiselect__loading-leave-active{opacity:0}.multiselect,.multiselect__input,.multiselect__single{font-family:inherit;font-size:16px;-ms-touch-action:manipulation;touch-action:manipulation}.multiselect{-webkit-box-sizing:content-box;box-sizing:content-box;display:block;position:relative;width:100%;min-height:40px;text-align:left;color:#35495e}.multiselect *{-webkit-box-sizing:border-box;box-sizing:border-box}.multiselect:focus{outline:none}.multiselect--disabled{background:#ededed;pointer-events:none;opacity:.6}.multiselect--active{z-index:50}.multiselect--active:not(.multiselect--above) .multiselect__current,.multiselect--active:not(.multiselect--above) .multiselect__input,.multiselect--active:not(.multiselect--above) .multiselect__tags{border-bottom-left-radius:0;border-bottom-right-radius:0}.multiselect--active .multiselect__select{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.multiselect--above.multiselect--active .multiselect__current,.multiselect--above.multiselect--active .multiselect__input,.multiselect--above.multiselect--active .multiselect__tags{border-top-left-radius:0;border-top-right-radius:0}.multiselect__input,.multiselect__single{position:relative;display:inline-block;min-height:20px;line-height:20px;border:none;border-radius:5px;background:#fff;padding:0 0 0 5px;width:100%;-webkit-transition:border .1s ease;transition:border .1s ease;-webkit-box-sizing:border-box;box-sizing:border-box;margin-bottom:8px;vertical-align:top}.multiselect__input::-webkit-input-placeholder{color:#35495e}.multiselect__input::-moz-placeholder{color:#35495e}.multiselect__input:-ms-input-placeholder{color:#35495e}.multiselect__input::-ms-input-placeholder{color:#35495e}.multiselect__input::placeholder{color:#35495e}.multiselect__tag~.multiselect__input,.multiselect__tag~.multiselect__single{width:auto}.multiselect__input:hover,.multiselect__single:hover{border-color:#cfcfcf}.multiselect__input:focus,.multiselect__single:focus{border-color:#a8a8a8;outline:none}.multiselect__single{padding-left:5px;margin-bottom:8px}.multiselect__tags-wrap{display:inline}.multiselect__tags{min-height:40px;display:block;padding:8px 40px 0 8px;border-radius:5px;border:1px solid #e8e8e8;background:#fff;font-size:14px}.multiselect__tag{position:relative;display:inline-block;padding:4px 26px 4px 10px;border-radius:5px;margin-right:10px;color:#fff;line-height:1;background:#41b883;margin-bottom:5px;white-space:nowrap;overflow:hidden;max-width:100%;text-overflow:ellipsis}.multiselect__tag-icon{cursor:pointer;margin-left:7px;position:absolute;right:0;top:0;bottom:0;font-weight:700;font-style:normal;width:22px;text-align:center;line-height:22px;-webkit-transition:all .2s ease;transition:all .2s ease;border-radius:5px}.multiselect__tag-icon:after{content:"\D7";color:#266d4d;font-size:14px}.multiselect__tag-icon:focus,.multiselect__tag-icon:hover{background:#369a6e}.multiselect__tag-icon:focus:after,.multiselect__tag-icon:hover:after{color:#fff}.multiselect__current{min-height:40px;overflow:hidden;padding:8px 30px 0 12px;white-space:nowrap;border-radius:5px;border:1px solid #e8e8e8}.multiselect__current,.multiselect__select{line-height:16px;-webkit-box-sizing:border-box;box-sizing:border-box;display:block;margin:0;text-decoration:none;cursor:pointer}.multiselect__select{position:absolute;width:40px;height:38px;right:1px;top:1px;padding:4px 8px;text-align:center;-webkit-transition:-webkit-transform .2s ease;transition:-webkit-transform .2s ease;transition:transform .2s ease;transition:transform .2s ease,-webkit-transform .2s ease}.multiselect__select:before{position:relative;right:0;top:65%;color:#999;margin-top:4px;border-color:#999 transparent transparent;border-style:solid;border-width:5px 5px 0;content:""}.multiselect__placeholder{color:#adadad;display:inline-block;margin-bottom:10px;padding-top:2px}.multiselect--active .multiselect__placeholder{display:none}.multiselect__content-wrapper{position:absolute;display:block;background:#fff;width:100%;max-height:240px;overflow:auto;border:1px solid #e8e8e8;border-top:none;border-bottom-left-radius:5px;border-bottom-right-radius:5px;z-index:50;-webkit-overflow-scrolling:touch}.multiselect__content{list-style:none;display:inline-block;padding:0;margin:0;min-width:100%;vertical-align:top}.multiselect--above .multiselect__content-wrapper{bottom:100%;border-bottom-left-radius:0;border-bottom-right-radius:0;border-top-left-radius:5px;border-top-right-radius:5px;border-bottom:none;border-top:1px solid #e8e8e8}.multiselect__content::webkit-scrollbar{display:none}.multiselect__element{display:block}.multiselect__option{display:block;padding:12px;min-height:40px;line-height:16px;text-decoration:none;text-transform:none;vertical-align:middle;position:relative;cursor:pointer;white-space:nowrap}.multiselect__option:after{top:0;right:0;position:absolute;line-height:40px;padding-right:12px;padding-left:20px;font-size:13px}.multiselect__option--highlight{background:#41b883;outline:none;color:#fff}.multiselect__option--highlight:after{content:attr(data-select);background:#41b883;color:#fff}.multiselect__option--selected{background:#f3f3f3;color:#35495e;font-weight:700}.multiselect__option--selected:after{content:attr(data-selected);color:silver;background:inherit}.multiselect__option--selected.multiselect__option--highlight{background:#ff6a6a;color:#fff}.multiselect__option--selected.multiselect__option--highlight:after{background:#ff6a6a;content:attr(data-deselect);color:#fff}.multiselect--disabled .multiselect__current,.multiselect--disabled .multiselect__select{background:#ededed;color:#a6a6a6}.multiselect__option--disabled{background:#ededed!important;color:#a6a6a6!important;cursor:text;pointer-events:none}.multiselect__option--group{background:#ededed;color:#35495e}.multiselect__option--group.multiselect__option--highlight{background:#35495e;color:#fff}.multiselect__option--group.multiselect__option--highlight:after{background:#35495e}.multiselect__option--disabled.multiselect__option--highlight{background:#dedede}.multiselect__option--group-selected.multiselect__option--highlight{background:#ff6a6a;color:#fff}.multiselect__option--group-selected.multiselect__option--highlight:after{background:#ff6a6a;content:attr(data-deselect);color:#fff}.multiselect-enter-active,.multiselect-leave-active{-webkit-transition:all .15s ease;transition:all .15s ease}.multiselect-enter,.multiselect-leave-active{opacity:0}.multiselect__strong{margin-bottom:8px;line-height:20px;display:inline-block;vertical-align:top}[dir=rtl] .multiselect{text-align:right}[dir=rtl] .multiselect__select{right:auto;left:1px}[dir=rtl] .multiselect__tags{padding:8px 8px 0 40px}[dir=rtl] .multiselect__content{text-align:right}[dir=rtl] .multiselect__option:after{right:auto;left:0}[dir=rtl] .multiselect__clear{right:auto;left:12px}[dir=rtl] .multiselect__spinner{right:auto;left:1px}@-webkit-keyframes spinning{0%{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(2turn);transform:rotate(2turn)}}@keyframes spinning{0%{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(2turn);transform:rotate(2turn)}} \ No newline at end of file diff --git a/src/www/index.html b/src/www/index.html index 72044728..0b313017 100644 --- a/src/www/index.html +++ b/src/www/index.html @@ -4,6 +4,7 @@ WireGuard + @@ -284,8 +285,13 @@
- + + + +